tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self.__lock = Lock() # initialize multiprocessing mutex lock 129 130 self.aliases = TKS_TICKER_ALIASES 131 """Some aliases instead official tickers. 132 133 See also: `TKSEnums.TKS_TICKER_ALIASES` 134 """ 135 136 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 137 138 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 139 140 self._ticker = "" 141 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 142 143 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 144 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 145 146 See also: `SearchByTicker()`, `SearchInstruments()`. 147 """ 148 149 self._figi = "" 150 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 151 152 See also: `SearchByFIGI()`, `SearchInstruments()`. 153 """ 154 155 self.depth = 1 156 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 157 158 See also: `GetCurrentPrices()`. 159 """ 160 161 self.server = r"https://invest-public-api.tinkoff.ru/rest" 162 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 163 164 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 165 """ 166 167 uLogger.debug("Broker API server: {}".format(self.server)) 168 169 self.timeout = 15 170 """Server operations timeout in seconds. Default: `15`. 171 172 See also: `SendAPIRequest()`. 173 """ 174 175 self.headers = { 176 "Content-Type": "application/json", 177 "accept": "application/json", 178 "Authorization": "Bearer {}".format(self.token), 179 "x-app-name": "Tim55667757.TKSBrokerAPI", 180 } 181 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 182 183 See also: `SendAPIRequest()`. 184 """ 185 186 self.body = None 187 """Request body which send to broker server. Default: `None`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.moreDebug = False 193 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 194 195 self.useHTMLReports = False 196 """ 197 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 198 199 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 200 """ 201 202 self.historyFile = None 203 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 204 205 See also: `History()`. 206 """ 207 208 self.htmlHistoryFile = "index.html" 209 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 210 211 See also: `ShowHistoryChart()`. 212 """ 213 214 self.instrumentsFile = "instruments.md" 215 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 216 217 See also: `ShowInstrumentsInfo()`. 218 """ 219 220 self.searchResultsFile = "search-results.md" 221 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 222 223 See also: `SearchInstruments()`. 224 """ 225 226 self.pricesFile = "prices.md" 227 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 228 229 See also: `GetListOfPrices()`. 230 """ 231 232 self.infoFile = "info.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 236 """ 237 238 self.bondsXLSXFile = "ext-bonds.xlsx" 239 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 240 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 241 242 See also: `ExtendBondsData()`. 243 """ 244 245 self.calendarFile = "calendar.md" 246 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 247 248 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 249 250 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 251 """ 252 253 self.overviewFile = "overview.md" 254 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 255 256 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 257 """ 258 259 self.overviewDigestFile = "overview-digest.md" 260 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 261 262 See also: `Overview()` with parameter `details="digest"`. 263 """ 264 265 self.overviewPositionsFile = "overview-positions.md" 266 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 267 268 See also: `Overview()` with parameter `details="positions"`. 269 """ 270 271 self.overviewOrdersFile = "overview-orders.md" 272 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 273 274 See also: `Overview()` with parameter `details="orders"`. 275 """ 276 277 self.overviewAnalyticsFile = "overview-analytics.md" 278 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 279 280 See also: `Overview()` with parameter `details="analytics"`. 281 """ 282 283 self.overviewBondsCalendarFile = "overview-calendar.md" 284 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 285 286 See also: `Overview()` with parameter `details="calendar"`. 287 """ 288 289 self.reportFile = "deals.md" 290 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 291 292 See also: `Deals()`. 293 """ 294 295 self.withdrawalLimitsFile = "limits.md" 296 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 297 298 See also: `OverviewLimits()` and `RequestLimits()`. 299 """ 300 301 self.userInfoFile = "user-info.md" 302 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 303 304 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 305 """ 306 307 self.userAccountsFile = "accounts.md" 308 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 309 310 See also: `OverviewAccounts()`, `RequestAccounts()`. 311 """ 312 313 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 314 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 315 316 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 317 318 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 319 """ 320 321 self.iList = None # init iList for raw instruments data 322 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 323 324 See also: `Listing()`, `DumpInstruments()`. 325 """ 326 327 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 328 if useCache: 329 if os.path.exists(self.iListDumpFile): 330 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 331 curTime = datetime.now(tzutc()) 332 333 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 334 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 335 336 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 337 338 else: 339 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 340 341 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 342 os.path.abspath(self.iListDumpFile), 343 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 344 )) 345 346 else: 347 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 348 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 349 350 else: 351 self.iList = self.Listing() # request new raw instruments data from broker server 352 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 353 354 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 355 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 356 357 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 358 """ 359 360 @property 361 def ticker(self) -> str: 362 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 return self._ticker 370 371 @ticker.setter 372 def ticker(self, value): 373 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 374 375 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 376 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 377 378 See also: `SearchByTicker()`, `SearchInstruments()`. 379 """ 380 self._ticker = str(value).upper() # Tickers may be upper case only 381 382 @property 383 def figi(self) -> str: 384 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 385 386 See also: `SearchByFIGI()`, `SearchInstruments()`. 387 """ 388 return self._figi 389 390 @figi.setter 391 def figi(self, value): 392 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 393 394 See also: `SearchByFIGI()`, `SearchInstruments()`. 395 """ 396 self._figi = str(value).upper() # FIGI may be upper case only 397 398 def _ParseJSON(self, rawData="{}") -> dict: 399 """ 400 Parse JSON from response string. 401 402 :param rawData: this is a string with JSON-formatted text. 403 :return: JSON (dictionary), parsed from server response string. 404 """ 405 responseJSON = json.loads(rawData) if rawData else {} 406 407 if self.moreDebug: 408 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 409 410 return responseJSON 411 412 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 413 """ 414 Send GET or POST request to broker server and receive JSON object. 415 416 self.header: must be defining with dictionary of headers. 417 self.body: if define then used as request body. None by default. 418 self.timeout: global request timeout, 15 seconds by default. 419 :param url: url with REST request. 420 :param reqType: send "GET" or "POST" request. "GET" by default. 421 :param retry: how many times retry after first request if an 5xx server errors occurred. 422 :param pause: sleep time in seconds between retries. 423 :return: response JSON (dictionary) from broker. 424 """ 425 if reqType.upper() not in ("GET", "POST"): 426 uLogger.error("You can define request type: `GET` or `POST`!") 427 raise Exception("Incorrect value") 428 429 if self.moreDebug: 430 uLogger.debug("Request parameters:") 431 uLogger.debug(" - REST API URL: {}".format(url)) 432 uLogger.debug(" - request type: {}".format(reqType)) 433 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 434 uLogger.debug(" - body:\n{}".format(self.body)) 435 436 # fast hack to avoid all operations with some tickers/FIGI 437 responseJSON = {} 438 oK = True 439 for item in self.exclude: 440 if item in url: 441 if self.moreDebug: 442 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 443 444 oK = False 445 break 446 447 if oK: 448 with self.__lock: # acquire the mutex lock 449 counter = 0 450 response = None 451 errMsg = "" 452 453 while not response and counter <= retry: 454 if reqType == "GET": 455 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 456 457 if reqType == "POST": 458 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 459 460 if self.moreDebug: 461 uLogger.debug("Response:") 462 uLogger.debug(" - status code: {}".format(response.status_code)) 463 uLogger.debug(" - reason: {}".format(response.reason)) 464 uLogger.debug(" - body length: {}".format(len(response.text))) 465 uLogger.debug(" - headers:\n{}".format(response.headers)) 466 467 # Server returns some headers: 468 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 469 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 470 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 471 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 472 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 473 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 474 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 475 sleep(rateLimitWait) 476 477 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 478 if 400 <= response.status_code < 500: 479 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 480 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 481 482 if "code" in response.text and "message" in response.text: 483 msgDict = self._ParseJSON(rawData=response.text) 484 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 485 486 counter = retry + 1 # do not retry for 4xx errors 487 488 if 500 <= response.status_code < 600: 489 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 490 uLogger.debug(" - not oK, {}".format(errMsg)) 491 492 if "code" in response.text and "message" in response.text: 493 errMsgDict = self._ParseJSON(rawData=response.text) 494 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 495 496 counter += 1 497 498 if counter <= retry: 499 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 500 sleep(pause) 501 502 responseJSON = self._ParseJSON(rawData=response.text) 503 504 if errMsg: 505 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 506 uLogger.error(" - not oK, {}".format(errMsg)) 507 508 return responseJSON 509 510 def _IUpdater(self, iType: str) -> tuple: 511 """ 512 Request instrument by type from server. See available API methods for instruments: 513 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 514 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 515 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 516 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 517 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 518 519 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 520 :return: tuple with iType name and list of available instruments of current type for defined user token. 521 """ 522 result = [] 523 524 if iType in TKS_INSTRUMENTS: 525 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 526 527 # all instruments have the same body in API v2 requests: 528 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 529 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 530 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 531 532 return iType, result 533 534 def _IWrapper(self, kwargs): 535 """ 536 Wrapper runs instrument's update method `_IUpdater()`. 537 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 538 """ 539 return self._IUpdater(**kwargs) 540 541 def Listing(self) -> dict: 542 """ 543 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 544 545 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 546 """ 547 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 548 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 549 550 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 551 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 552 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 553 554 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 555 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 556 poolUpdater.close() # close the thread pool 557 poolUpdater.join() # wait a moment until all data returns from threads 558 559 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 560 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 561 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 562 563 # calculate minimum price increment (step) for all instruments and set up instrument's type: 564 for iType in iList.keys(): 565 for ticker in iList[iType]: 566 iList[iType][ticker]["type"] = iType 567 568 if "minPriceIncrement" in iList[iType][ticker].keys(): 569 iList[iType][ticker]["step"] = NanoToFloat( 570 iList[iType][ticker]["minPriceIncrement"]["units"], 571 iList[iType][ticker]["minPriceIncrement"]["nano"], 572 ) 573 574 else: 575 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 576 577 return iList 578 579 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 580 """ 581 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 582 583 See also: `DumpInstruments()`, `Listing()`. 584 585 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 586 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 587 """ 588 if self.iListDumpFile is None or not self.iListDumpFile: 589 uLogger.error("Output name of dump file must be defined!") 590 raise Exception("Filename required") 591 592 if not self.iList or forceUpdate: 593 self.iList = self.Listing() 594 595 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 596 597 # Save as XLSX with separated sheets for every type of instruments: 598 with pd.ExcelWriter( 599 path=xlsxDumpFile, 600 date_format=TKS_DATE_FORMAT, 601 datetime_format=TKS_DATE_TIME_FORMAT, 602 mode="w", 603 ) as writer: 604 for iType in TKS_INSTRUMENTS: 605 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 606 df = df[sorted(df)] # sorted by column names 607 df = df.applymap( 608 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 609 na_action="ignore", 610 ) # converting numbers from nano-type to float in every cell 611 df.to_excel( 612 writer, 613 sheet_name=iType, 614 encoding="UTF-8", 615 freeze_panes=(1, 1), 616 ) # saving as XLSX-file with freeze first row and column as headers 617 618 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 619 620 def DumpInstruments(self, forceUpdate: bool = True) -> str: 621 """ 622 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 623 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 624 625 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 626 627 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 628 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 629 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 630 """ 631 if self.iListDumpFile is None or not self.iListDumpFile: 632 uLogger.error("Output name of dump file must be defined!") 633 raise Exception("Filename required") 634 635 if not self.iList or forceUpdate: 636 self.iList = self.Listing() 637 638 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 639 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 640 fH.write(jsonDump) 641 642 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 643 644 return jsonDump 645 646 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 647 """ 648 Show information about one instrument defined by json data and prints it in Markdown format. 649 650 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 651 652 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 653 :param show: if `True` then also printing information about instrument and its current price. 654 :return: multilines text in Markdown format with information about one instrument. 655 """ 656 splitLine = "| | |\n" 657 infoText = "" 658 659 if iJSON is not None and iJSON and isinstance(iJSON, dict): 660 info = [ 661 "# Main information\n\n", 662 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 663 "| Parameters | Values |\n", 664 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 665 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 666 "| Full name: | {:<54} |\n".format(iJSON["name"]), 667 ] 668 669 if "sector" in iJSON.keys() and iJSON["sector"]: 670 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 671 672 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 673 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 674 675 info.extend([ 676 splitLine, 677 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 678 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 679 ]) 680 681 if "isin" in iJSON.keys() and iJSON["isin"]: 682 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 683 684 if "classCode" in iJSON.keys(): 685 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 686 687 info.extend([ 688 splitLine, 689 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 690 splitLine, 691 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 692 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 693 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 694 ]) 695 696 if iJSON["figi"]: 697 self._figi = iJSON["figi"] 698 iJSON = iJSON | self.RequestTradingStatus() 699 700 info.extend([ 701 splitLine, 702 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 703 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 704 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 705 ]) 706 707 info.append(splitLine) 708 709 if "type" in iJSON.keys() and iJSON["type"]: 710 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 711 712 if "shareType" in iJSON.keys() and iJSON["shareType"]: 713 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 714 715 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 716 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 717 718 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 719 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 720 721 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 722 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 723 724 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 725 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 726 727 if "focusType" in iJSON.keys() and iJSON["focusType"]: 728 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 729 730 if "assetType" in iJSON.keys() and iJSON["assetType"]: 731 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 732 733 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 734 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 735 736 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 737 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 738 739 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 740 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 741 742 if "currency" in iJSON.keys(): 743 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 744 745 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 746 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 747 748 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 749 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 750 751 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 752 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 753 754 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 755 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 756 757 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 758 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 759 760 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 761 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 762 763 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 764 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 765 766 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 767 info.append("| Perpetual bond: | Yes |\n") 768 769 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 770 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 771 772 iExt = None 773 if iJSON["type"] == "Bonds": 774 info.extend([ 775 splitLine, 776 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 777 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 778 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 779 iJSON["nominal"]["currency"], 780 )), 781 ]) 782 783 if "floatingCouponFlag" in iJSON.keys(): 784 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 785 786 if "amortizationFlag" in iJSON.keys(): 787 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 788 789 info.append(splitLine) 790 791 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 792 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 793 794 if iJSON["figi"]: 795 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 796 797 info.extend([ 798 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 799 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 800 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 801 ]) 802 803 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 804 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 805 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 806 iJSON["aciValue"]["currency"] 807 ))) 808 809 if "currentPrice" in iJSON.keys(): 810 info.append(splitLine) 811 812 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 813 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 814 815 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 816 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 817 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 818 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 819 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 820 821 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 822 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 823 824 info.extend([ 825 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 826 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 827 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 828 )), 829 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 830 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 831 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 832 )), 833 "| Changes between last deal price and last close | {:<54} |\n".format( 834 "{:.2f}%{}".format( 835 iJSON["currentPrice"]["changes"], 836 " ({}{:.2f} {})".format( 837 "+" if bondChangesDelta > 0 else "", 838 bondChangesDelta, 839 aciCurrency 840 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 841 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 842 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 843 currency 844 ), 845 ) 846 ), 847 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 848 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 849 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 850 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 851 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 852 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 853 )), 854 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 855 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 856 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 857 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 858 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 859 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 860 )), 861 ]) 862 863 if "lot" in iJSON.keys(): 864 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 865 866 if "step" in iJSON.keys() and iJSON["step"] != 0: 867 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 868 869 # Add bond payment calendar: 870 if iJSON["type"] == "Bonds": 871 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 872 info.extend(["\n#", strCalendar]) 873 874 infoText += "".join(info) 875 876 if show: 877 uLogger.info("{}".format(infoText)) 878 879 else: 880 uLogger.debug("{}".format(infoText)) 881 882 if self.infoFile is not None: 883 with open(self.infoFile, "w", encoding="UTF-8") as fH: 884 fH.write(infoText) 885 886 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 887 888 if self.useHTMLReports: 889 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 890 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 891 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 892 893 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 894 895 return infoText 896 897 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 898 """ 899 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 900 901 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 902 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 903 :return: JSON formatted data with information about instrument. 904 """ 905 tickerJSON = {} 906 if self.moreDebug: 907 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 908 909 if not self._ticker: 910 uLogger.warning("self._ticker variable is not be empty!") 911 912 else: 913 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 914 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 915 raise Exception("Instrument not allowed") 916 917 if not self.iList: 918 self.iList = self.Listing() 919 920 if self._ticker in self.iList["Shares"].keys(): 921 tickerJSON = self.iList["Shares"][self._ticker] 922 if self.moreDebug: 923 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 924 925 elif self._ticker in self.iList["Currencies"].keys(): 926 tickerJSON = self.iList["Currencies"][self._ticker] 927 if self.moreDebug: 928 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 929 930 elif self._ticker in self.iList["Bonds"].keys(): 931 tickerJSON = self.iList["Bonds"][self._ticker] 932 if self.moreDebug: 933 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 934 935 elif self._ticker in self.iList["Etfs"].keys(): 936 tickerJSON = self.iList["Etfs"][self._ticker] 937 if self.moreDebug: 938 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 939 940 elif self._ticker in self.iList["Futures"].keys(): 941 tickerJSON = self.iList["Futures"][self._ticker] 942 if self.moreDebug: 943 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 944 945 if tickerJSON: 946 self._figi = tickerJSON["figi"] 947 948 if requestPrice: 949 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 950 951 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 952 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 953 954 else: 955 tickerJSON["currentPrice"]["changes"] = 0 956 957 if show: 958 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 959 960 else: 961 if show: 962 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 963 964 return tickerJSON 965 966 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 967 """ 968 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 969 970 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 971 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 972 :return: JSON formatted data with information about instrument. 973 """ 974 figiJSON = {} 975 if self.moreDebug: 976 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 977 978 if not self._figi: 979 uLogger.warning("self._figi variable is not be empty!") 980 981 else: 982 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 983 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 984 raise Exception("Instrument not allowed") 985 986 if not self.iList: 987 self.iList = self.Listing() 988 989 for item in self.iList["Shares"].keys(): 990 if self._figi == self.iList["Shares"][item]["figi"]: 991 figiJSON = self.iList["Shares"][item] 992 993 if self.moreDebug: 994 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 995 996 break 997 998 if not figiJSON: 999 for item in self.iList["Currencies"].keys(): 1000 if self._figi == self.iList["Currencies"][item]["figi"]: 1001 figiJSON = self.iList["Currencies"][item] 1002 1003 if self.moreDebug: 1004 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1005 1006 break 1007 1008 if not figiJSON: 1009 for item in self.iList["Bonds"].keys(): 1010 if self._figi == self.iList["Bonds"][item]["figi"]: 1011 figiJSON = self.iList["Bonds"][item] 1012 1013 if self.moreDebug: 1014 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1015 1016 break 1017 1018 if not figiJSON: 1019 for item in self.iList["Etfs"].keys(): 1020 if self._figi == self.iList["Etfs"][item]["figi"]: 1021 figiJSON = self.iList["Etfs"][item] 1022 1023 if self.moreDebug: 1024 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1025 1026 break 1027 1028 if not figiJSON: 1029 for item in self.iList["Futures"].keys(): 1030 if self._figi == self.iList["Futures"][item]["figi"]: 1031 figiJSON = self.iList["Futures"][item] 1032 1033 if self.moreDebug: 1034 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1035 1036 break 1037 1038 if figiJSON: 1039 self._figi = figiJSON["figi"] 1040 self._ticker = figiJSON["ticker"] 1041 1042 if requestPrice: 1043 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1044 1045 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1046 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1047 1048 else: 1049 figiJSON["currentPrice"]["changes"] = 0 1050 1051 if show: 1052 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1053 1054 else: 1055 if show: 1056 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1057 1058 return figiJSON 1059 1060 def GetCurrentPrices(self, show: bool = True) -> dict: 1061 """ 1062 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1063 `{"buy": [{"price": 1243.8, "quantity": 193}, 1064 {"price": 1244.0, "quantity": 168}, 1065 {"price": 1244.8, "quantity": 5}, 1066 {"price": 1245.0, "quantity": 61}, 1067 {"price": 1245.4, "quantity": 60}], 1068 "sell": [{"price": 1243.6, "quantity": 8}, 1069 {"price": 1242.6, "quantity": 10}, 1070 {"price": 1242.4, "quantity": 18}, 1071 {"price": 1242.2, "quantity": 50}, 1072 {"price": 1242.0, "quantity": 113}], 1073 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1074 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1075 - sell: list of dicts with Buyers prices, 1076 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1077 - quantity: volume value by current price in lots, 1078 - limitUp: current trade session limit price, maximum, 1079 - limitDown: current trade session limit price, minimum, 1080 - lastPrice: last deal price of the instrument, 1081 - closePrice: previous trade session close price of the instrument. 1082 1083 See also: `SearchByTicker()` and `SearchByFIGI()`. 1084 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1085 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1086 1087 :param show: if `True` then print DOM to log and console. 1088 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1089 If an error occurred then returns an empty record: 1090 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1091 """ 1092 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1093 1094 if self.depth < 1: 1095 uLogger.error("Depth of Market (DOM) must be >=1!") 1096 raise Exception("Incorrect value") 1097 1098 if not (self._ticker or self._figi): 1099 uLogger.error("self._ticker or self._figi variables must be defined!") 1100 raise Exception("Ticker or FIGI required") 1101 1102 if self._ticker and not self._figi: 1103 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1104 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1105 1106 if not self._ticker and self._figi: 1107 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1108 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1109 1110 if not self._figi: 1111 uLogger.error("FIGI is not defined!") 1112 raise Exception("Ticker or FIGI required") 1113 1114 else: 1115 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1116 1117 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1118 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1119 self.body = str({"figi": self._figi, "depth": self.depth}) 1120 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1121 1122 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1123 # list of dicts with sellers orders: 1124 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1125 1126 # list of dicts with buyers orders: 1127 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1128 1129 # max price of instrument at this time: 1130 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1131 1132 # min price of instrument at this time: 1133 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1134 1135 # last price of deal with instrument: 1136 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1137 1138 # last close price of instrument: 1139 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1140 1141 else: 1142 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1143 uLogger.debug("Server response: {}".format(pricesResponse)) 1144 1145 if show: 1146 if prices["buy"] or prices["sell"]: 1147 info = [ 1148 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1149 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1150 self._ticker, 1151 self._figi, 1152 self.depth, 1153 ), 1154 "-" * 60, "\n", 1155 " Orders of Buyers | Orders of Sellers\n", 1156 "-" * 60, "\n", 1157 " Sell prices (volumes) | Buy prices (volumes)\n", 1158 "-" * 60, "\n", 1159 ] 1160 1161 if not prices["buy"]: 1162 info.append(" | No orders!\n") 1163 sumBuy = 0 1164 1165 else: 1166 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1167 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1168 for item in maxMinSorted: 1169 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1170 1171 if not prices["sell"]: 1172 info.append("No orders! |\n") 1173 sumSell = 0 1174 1175 else: 1176 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1177 for item in prices["sell"]: 1178 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1179 1180 info.extend([ 1181 "-" * 60, "\n", 1182 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1183 "-" * 60, "\n", 1184 ]) 1185 1186 infoText = "".join(info) 1187 1188 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1189 1190 else: 1191 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1192 1193 return prices 1194 1195 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1196 """ 1197 This method get and show information about all available broker instruments for current user account. 1198 If `instrumentsFile` string is not empty then also save information to this file. 1199 1200 :param show: if `True` then print results to console, if `False` — print only to file. 1201 :return: multi-lines string with all available broker instruments 1202 """ 1203 if not self.iList: 1204 self.iList = self.Listing() 1205 1206 info = [ 1207 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1208 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1209 ] 1210 1211 # add instruments count by type: 1212 for iType in self.iList.keys(): 1213 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1214 1215 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1216 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1217 1218 # generating info tables with all instruments by type: 1219 for iType in self.iList.keys(): 1220 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1221 1222 for instrument in self.iList[iType].keys(): 1223 iName = self.iList[iType][instrument]["name"] # instrument's name 1224 if len(iName) > 57: 1225 iName = "{}...".format(iName[:54]) # right trim for a long string 1226 1227 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1228 self.iList[iType][instrument]["ticker"], 1229 iName, 1230 self.iList[iType][instrument]["figi"], 1231 self.iList[iType][instrument]["currency"], 1232 self.iList[iType][instrument]["lot"], 1233 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1234 )) 1235 1236 infoText = "".join(info) 1237 1238 if show: 1239 uLogger.info(infoText) 1240 1241 if self.instrumentsFile: 1242 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1243 fH.write(infoText) 1244 1245 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1246 1247 if self.useHTMLReports: 1248 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1249 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1250 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1251 1252 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1253 1254 return infoText 1255 1256 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1257 """ 1258 This method search and show information about instruments by part of its ticker, FIGI or name. 1259 If `searchResultsFile` string is not empty then also save information to this file. 1260 1261 :param pattern: string with part of ticker, FIGI or instrument's name. 1262 :param show: if `True` then print results to console, if `False` — return list of result only. 1263 :return: list of dictionaries with all found instruments. 1264 """ 1265 if not self.iList: 1266 self.iList = self.Listing() 1267 1268 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1269 compiledPattern = re.compile(pattern, re.IGNORECASE) 1270 1271 for iType in self.iList: 1272 for instrument in self.iList[iType].values(): 1273 searchResult = compiledPattern.search(" ".join( 1274 [instrument["ticker"], instrument["figi"], instrument["name"]] 1275 )) 1276 1277 if searchResult: 1278 searchResults[iType][instrument["ticker"]] = instrument 1279 1280 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1281 info = [ 1282 "# Search results\n\n", 1283 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1284 "* **Search pattern:** [{}]\n".format(pattern), 1285 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1286 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1287 ] 1288 infoShort = info[:] 1289 1290 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1291 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1292 skippedLine = "| ... | ... | ... | ... |\n" 1293 1294 if resultsLen == 0: 1295 info.append("\nNo results\n") 1296 infoShort.append("\nNo results\n") 1297 uLogger.warning("No results. Try changing your search pattern.") 1298 1299 else: 1300 for iType in searchResults: 1301 iTypeValuesCount = len(searchResults[iType].values()) 1302 if iTypeValuesCount > 0: 1303 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1304 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 1306 for instrument in searchResults[iType].values(): 1307 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1308 instrument["type"], 1309 instrument["ticker"], 1310 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1311 instrument["figi"], 1312 )) 1313 1314 if iTypeValuesCount <= 5: 1315 infoShort.extend(info[-iTypeValuesCount:]) 1316 1317 else: 1318 infoShort.extend(info[-5:]) 1319 infoShort.append(skippedLine) 1320 1321 infoText = "".join(info) 1322 infoTextShort = "".join(infoShort) 1323 1324 if show: 1325 uLogger.info(infoTextShort) 1326 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1327 1328 if self.searchResultsFile: 1329 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1330 fH.write(infoText) 1331 1332 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1333 1334 if self.useHTMLReports: 1335 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1336 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1337 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1338 1339 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1340 1341 return searchResults 1342 1343 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1344 """ 1345 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1346 1347 :param instruments: list of strings with tickers or FIGIs. 1348 :return: list with unique instrument FIGIs only. 1349 """ 1350 requestedInstruments = [] 1351 for iName in instruments: 1352 if iName not in self.aliases.keys(): 1353 if iName not in requestedInstruments: 1354 requestedInstruments.append(iName) 1355 1356 else: 1357 if iName not in requestedInstruments: 1358 if self.aliases[iName] not in requestedInstruments: 1359 requestedInstruments.append(self.aliases[iName]) 1360 1361 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1362 1363 onlyUniqueFIGIs = [] 1364 for iName in requestedInstruments: 1365 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1366 continue 1367 1368 self._ticker = iName 1369 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1370 1371 if not iData: 1372 self._ticker = "" 1373 self._figi = iName 1374 1375 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1376 1377 if not iData: 1378 self._figi = "" 1379 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1380 1381 if iData and iData["figi"] not in onlyUniqueFIGIs: 1382 onlyUniqueFIGIs.append(iData["figi"]) 1383 1384 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1385 1386 return onlyUniqueFIGIs 1387 1388 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1389 """ 1390 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1391 1392 See limits: https://tinkoff.github.io/investAPI/limits/ 1393 1394 If `pricesFile` string is not empty then also save information to this file. 1395 1396 :param instruments: list of strings with tickers or FIGIs. 1397 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1398 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1399 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1400 """ 1401 if instruments is None or not instruments: 1402 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1403 raise Exception("Ticker or FIGI required") 1404 1405 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1406 1407 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1408 1409 iList = [] # trying to get info and current prices about all unique instruments: 1410 for self._figi in onlyUniqueFIGIs: 1411 iData = self.SearchByFIGI(requestPrice=True) 1412 iList.append(iData) 1413 1414 self.ShowListOfPrices(iList, show) 1415 1416 return iList 1417 1418 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1419 """ 1420 Show table contains current prices of given instruments. 1421 1422 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1423 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1424 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1425 :return: multilines text in Markdown format as a table contains current prices. 1426 """ 1427 infoText = "" 1428 1429 if show or self.pricesFile: 1430 info = [ 1431 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1432 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1433 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1434 ] 1435 1436 for item in iList: 1437 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1438 item["ticker"], 1439 item["figi"], 1440 item["type"], 1441 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1442 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1443 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1444 "{} / {}".format( 1445 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1446 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1447 ), 1448 "{} / {}".format( 1449 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1450 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1451 ), 1452 item["currency"], 1453 )) 1454 1455 infoText = "".join(info) 1456 1457 if show: 1458 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1459 1460 if self.pricesFile: 1461 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1462 fH.write(infoText) 1463 1464 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1465 1466 if self.useHTMLReports: 1467 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1468 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1469 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1470 1471 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1472 1473 return infoText 1474 1475 def RequestTradingStatus(self) -> dict: 1476 """ 1477 Requesting trading status for the instrument defined by `figi` variable. 1478 1479 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1480 1481 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1482 1483 :return: dictionary with trading status attributes. Response example: 1484 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1485 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1486 """ 1487 if self._figi is None or not self._figi: 1488 uLogger.error("Variable `figi` must be defined for using this method!") 1489 raise Exception("FIGI required") 1490 1491 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1492 1493 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1494 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1495 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1496 1497 if self.moreDebug: 1498 uLogger.debug("Records about current trading status successfully received") 1499 1500 return tradingStatus 1501 1502 def RequestPortfolio(self) -> dict: 1503 """ 1504 Requesting actual user's portfolio for current `accountId`. 1505 1506 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1507 1508 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1509 1510 :return: dictionary with user's portfolio. 1511 """ 1512 if self.accountId is None or not self.accountId: 1513 uLogger.error("Variable `accountId` must be defined for using this method!") 1514 raise Exception("Account ID required") 1515 1516 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1517 1518 self.body = str({"accountId": self.accountId}) 1519 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1520 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1521 1522 if self.moreDebug: 1523 uLogger.debug("Records about user's portfolio successfully received") 1524 1525 return rawPortfolio 1526 1527 def RequestPositions(self) -> dict: 1528 """ 1529 Requesting open positions by currencies and instruments for current `accountId`. 1530 1531 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1532 1533 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1534 1535 :return: dictionary with open positions by instruments. 1536 """ 1537 if self.accountId is None or not self.accountId: 1538 uLogger.error("Variable `accountId` must be defined for using this method!") 1539 raise Exception("Account ID required") 1540 1541 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1542 1543 self.body = str({"accountId": self.accountId}) 1544 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1545 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1546 1547 if self.moreDebug: 1548 uLogger.debug("Records about current open positions successfully received") 1549 1550 return rawPositions 1551 1552 def RequestPendingOrders(self) -> list: 1553 """ 1554 Requesting current actual pending limit orders for current `accountId`. 1555 1556 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1557 1558 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1559 1560 :return: list of dictionaries with pending limit orders. 1561 """ 1562 if self.accountId is None or not self.accountId: 1563 uLogger.error("Variable `accountId` must be defined for using this method!") 1564 raise Exception("Account ID required") 1565 1566 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1567 1568 self.body = str({"accountId": self.accountId}) 1569 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1570 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1571 1572 if "orders" in rawResponse.keys(): 1573 rawOrders = rawResponse["orders"] 1574 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1575 1576 else: 1577 rawOrders = [] 1578 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1579 1580 return rawOrders 1581 1582 def RequestStopOrders(self) -> list: 1583 """ 1584 Requesting current actual stop orders for current `accountId`. 1585 1586 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1587 1588 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1589 1590 :return: list of dictionaries with stop orders. 1591 """ 1592 if self.accountId is None or not self.accountId: 1593 uLogger.error("Variable `accountId` must be defined for using this method!") 1594 raise Exception("Account ID required") 1595 1596 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1597 1598 self.body = str({"accountId": self.accountId}) 1599 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1600 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1601 1602 if "stopOrders" in rawResponse.keys(): 1603 rawStopOrders = rawResponse["stopOrders"] 1604 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1605 1606 else: 1607 rawStopOrders = [] 1608 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1609 1610 return rawStopOrders 1611 1612 def Overview(self, show: bool = False, details: str = "full") -> dict: 1613 """ 1614 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1615 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1616 and `overviewBondsCalendarFile` are defined then also save information to file. 1617 1618 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1619 many requests about the state of the portfolio, and then, based on the received data, a large number 1620 of calculation and statistics are collected. 1621 1622 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1623 :param details: how detailed should the information be? 1624 - `full` — shows full available information about portfolio status (by default), 1625 - `positions` — shows only open positions, 1626 - `orders` — shows only sections of open limits and stop orders. 1627 - `digest` — show a short digest of the portfolio status, 1628 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1629 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1630 :return: dictionary with client's raw portfolio and some statistics. 1631 """ 1632 if self.accountId is None or not self.accountId: 1633 uLogger.error("Variable `accountId` must be defined for using this method!") 1634 raise Exception("Account ID required") 1635 1636 view = { 1637 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1638 "headers": {}, # list of dictionaries, response headers without "positions" section 1639 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1640 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1641 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1642 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1643 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1644 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1645 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1646 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1647 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1648 }, 1649 "stat": { # --- some statistics calculated using "raw" sections: 1650 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1651 "availableRUB": 0., # available rubles (without other currencies) 1652 "blockedRUB": 0., # blocked sum in Russian Rouble 1653 "totalChangesRUB": 0., # changes for all open trades in RUB 1654 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1655 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1656 "sharesCostRUB": 0., # costs of all shares in RUB 1657 "bondsCostRUB": 0., # costs of all bonds in RUB 1658 "etfsCostRUB": 0., # costs of all etfs in RUB 1659 "futuresCostRUB": 0., # costs of all futures in RUB 1660 "Currencies": [], # list of dictionaries of all currencies statistics 1661 "Shares": [], # list of dictionaries of all shares statistics 1662 "Bonds": [], # list of dictionaries of all bonds statistics 1663 "Etfs": [], # list of dictionaries of all etfs statistics 1664 "Futures": [], # list of dictionaries of all futures statistics 1665 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1666 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1667 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1668 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1669 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1670 }, 1671 "analytics": { # --- some analytics of portfolio: 1672 "distrByAssets": {}, # portfolio distribution by assets 1673 "distrByCompanies": {}, # portfolio distribution by companies 1674 "distrBySectors": {}, # portfolio distribution by sectors 1675 "distrByCurrencies": {}, # portfolio distribution by currencies 1676 "distrByCountries": {}, # portfolio distribution by countries 1677 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1678 } 1679 } 1680 1681 details = details.lower() 1682 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1683 if details not in availableDetails: 1684 details = "full" 1685 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1686 1687 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1688 1689 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1690 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1691 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1692 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1693 1694 # save response headers without "positions" section: 1695 for key in portfolioResponse.keys(): 1696 if key != "positions": 1697 view["raw"]["headers"][key] = portfolioResponse[key] 1698 1699 else: 1700 continue 1701 1702 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1703 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1704 for item in portfolioResponse["positions"]: 1705 if item["instrumentType"] == "currency": 1706 self._figi = item["figi"] 1707 curr = self.SearchByFIGI(requestPrice=False) 1708 1709 # current price of currency in RUB: 1710 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1711 "name": curr["name"], 1712 "currentPrice": NanoToFloat( 1713 item["currentPrice"]["units"], 1714 item["currentPrice"]["nano"] 1715 ), 1716 } 1717 1718 view["raw"]["Currencies"].append(item) 1719 1720 elif item["instrumentType"] == "share": 1721 view["raw"]["Shares"].append(item) 1722 1723 elif item["instrumentType"] == "bond": 1724 view["raw"]["Bonds"].append(item) 1725 1726 elif item["instrumentType"] == "etf": 1727 view["raw"]["Etfs"].append(item) 1728 1729 elif item["instrumentType"] == "futures": 1730 view["raw"]["Futures"].append(item) 1731 1732 else: 1733 continue 1734 1735 # how many volume of currencies (by ISO currency name) are blocked: 1736 for item in view["raw"]["positions"]["blocked"]: 1737 blocked = NanoToFloat(item["units"], item["nano"]) 1738 if blocked > 0: 1739 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1740 1741 # how many volume of instruments (by FIGI) are blocked: 1742 for item in view["raw"]["positions"]["securities"]: 1743 blocked = int(item["blocked"]) 1744 if blocked > 0: 1745 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1746 1747 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1748 1749 if "rub" in allBlocked.keys(): 1750 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1751 1752 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1753 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1754 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1755 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1756 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1757 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1758 view["stat"]["portfolioCostRUB"] = sum([ 1759 view["stat"]["allCurrenciesCostRUB"], 1760 view["stat"]["sharesCostRUB"], 1761 view["stat"]["bondsCostRUB"], 1762 view["stat"]["etfsCostRUB"], 1763 view["stat"]["futuresCostRUB"], 1764 ]) 1765 1766 # --- calculating some portfolio statistics: 1767 byComp = {} # distribution by companies 1768 bySect = {} # distribution by sectors 1769 byCurr = {} # distribution by currencies (include RUB) 1770 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1771 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1772 1773 for item in portfolioResponse["positions"]: 1774 self._figi = item["figi"] 1775 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1776 1777 if instrument: 1778 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1779 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1780 1781 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1782 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1783 1784 else: 1785 blocked = 0 1786 1787 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1788 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1789 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1790 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1791 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1792 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1793 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1794 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1795 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1796 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1797 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1798 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1799 1800 statData = { 1801 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1802 "ticker": instrument["ticker"], # ticker by FIGI 1803 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1804 "volume": volume, # available volume of instrument 1805 "lots": lots, # volume in lots of instrument 1806 "direction": direction, # direction of an instrument's position: short or long 1807 "blocked": blocked, # blocked volume of currency or instrument 1808 "currentPrice": curPrice, # current instrument's price in basic asset 1809 "average": average, # current average position price 1810 "cost": cost, # current cost of all volume of instrument in basic asset 1811 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1812 "costRUB": costRUB, # cost of instrument in ruble 1813 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1814 "profit": profit, # expected profit at current moment 1815 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1816 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1817 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1818 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1819 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1820 "step": instrument["step"], # minimum price increment 1821 } 1822 1823 # adding distribution by unique countries: 1824 if statData["country"] not in byCountry.keys(): 1825 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1826 1827 else: 1828 byCountry[statData["country"]]["cost"] += costRUB 1829 byCountry[statData["country"]]["percent"] += percentCostRUB 1830 1831 if item["instrumentType"] != "currency": 1832 # adding distribution by unique companies: 1833 if statData["name"]: 1834 if statData["name"] not in byComp.keys(): 1835 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1836 1837 else: 1838 byComp[statData["name"]]["cost"] += costRUB 1839 byComp[statData["name"]]["percent"] += percentCostRUB 1840 1841 # adding distribution by unique sectors: 1842 if statData["sector"] not in bySect.keys(): 1843 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1844 1845 else: 1846 bySect[statData["sector"]]["cost"] += costRUB 1847 bySect[statData["sector"]]["percent"] += percentCostRUB 1848 1849 # adding distribution by unique currencies: 1850 if currency not in byCurr.keys(): 1851 byCurr[currency] = { 1852 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1853 "cost": costRUB, 1854 "percent": percentCostRUB 1855 } 1856 1857 else: 1858 byCurr[currency]["cost"] += costRUB 1859 byCurr[currency]["percent"] += percentCostRUB 1860 1861 # saving statistics for every instrument: 1862 if item["instrumentType"] == "currency": 1863 view["stat"]["Currencies"].append(statData) 1864 1865 # update dict with free funds for trading (total - blocked) by currencies 1866 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1867 view["stat"]["funds"][currency] = { 1868 "total": volume, 1869 "totalCostRUB": costRUB, # total volume cost in rubles 1870 "free": volume - blocked, 1871 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1872 } 1873 1874 elif item["instrumentType"] == "share": 1875 view["stat"]["Shares"].append(statData) 1876 1877 elif item["instrumentType"] == "bond": 1878 view["stat"]["Bonds"].append(statData) 1879 1880 elif item["instrumentType"] == "etf": 1881 view["stat"]["Etfs"].append(statData) 1882 1883 elif item["instrumentType"] == "Futures": 1884 view["stat"]["Futures"].append(statData) 1885 1886 else: 1887 continue 1888 1889 # total changes in Russian Ruble: 1890 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1891 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1892 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1893 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1894 view["stat"]["funds"]["rub"] = { 1895 "total": view["stat"]["availableRUB"], 1896 "totalCostRUB": view["stat"]["availableRUB"], 1897 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1898 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1899 } 1900 1901 # --- pending limit orders sector data: 1902 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1903 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1904 1905 for item in view["raw"]["orders"]: 1906 self._figi = item["figi"] 1907 1908 if item["figi"] not in uniquePendingOrdersFIGIs: 1909 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1910 1911 uniquePendingOrdersFIGIs.append(item["figi"]) 1912 uniquePendingOrders[item["figi"]] = instrument 1913 1914 else: 1915 instrument = uniquePendingOrders[item["figi"]] 1916 1917 if instrument: 1918 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1919 orderType = TKS_ORDER_TYPES[item["orderType"]] 1920 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1921 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1922 1923 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1924 if item["direction"] == "ORDER_DIRECTION_BUY": 1925 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1926 1927 else: 1928 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1929 1930 # requested price for order execution: 1931 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1932 1933 # necessary changes in percent to reach target from current price: 1934 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1935 1936 view["stat"]["orders"].append({ 1937 "orderID": item["orderId"], # orderId number parameter of current order 1938 "figi": item["figi"], # FIGI identification 1939 "ticker": instrument["ticker"], # ticker name by FIGI 1940 "lotsRequested": item["lotsRequested"], # requested lots value 1941 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1942 "currentPrice": lastPrice, # current instrument's price for defined action 1943 "targetPrice": target, # requested price for order execution in base currency 1944 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1945 "percentChanges": changes, # changes in percent to target from current price 1946 "currency": item["currency"], # instrument's currency name 1947 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1948 "type": orderType, # type of order from TKS_ORDER_TYPES 1949 "status": orderState, # order status from TKS_ORDER_STATES 1950 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1951 }) 1952 1953 # --- stop orders sector data: 1954 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1955 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1956 1957 for item in view["raw"]["stopOrders"]: 1958 self._figi = item["figi"] 1959 1960 if item["figi"] not in uniqueStopOrdersFIGIs: 1961 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1962 1963 uniqueStopOrdersFIGIs.append(item["figi"]) 1964 uniqueStopOrders[item["figi"]] = instrument 1965 1966 else: 1967 instrument = uniqueStopOrders[item["figi"]] 1968 1969 if instrument: 1970 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1971 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1972 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1973 1974 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1975 if "expirationTime" in item.keys(): 1976 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1977 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1978 1979 else: 1980 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1981 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1982 1983 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1984 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1985 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1986 1987 else: 1988 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1989 1990 # requested price when stop-order executed: 1991 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1992 1993 # price for limit-order, set up when stop-order executed: 1994 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1995 1996 # necessary changes in percent to reach target from current price: 1997 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1998 1999 view["stat"]["stopOrders"].append({ 2000 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2001 "figi": item["figi"], # FIGI identification 2002 "ticker": instrument["ticker"], # ticker name by FIGI 2003 "lotsRequested": item["lotsRequested"], # requested lots value 2004 "currentPrice": lastPrice, # current instrument's price for defined action 2005 "targetPrice": target, # requested price for stop-order execution in base currency 2006 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2007 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2008 "percentChanges": changes, # changes in percent to target from current price 2009 "currency": item["currency"], # instrument's currency name 2010 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2011 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2012 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2013 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2014 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2015 }) 2016 2017 # --- calculating data for analytics section: 2018 # portfolio distribution by assets: 2019 view["analytics"]["distrByAssets"] = { 2020 "Ruble": { 2021 "uniques": 1, 2022 "cost": view["stat"]["availableRUB"], 2023 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Currencies": { 2026 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2027 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2028 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Shares": { 2031 "uniques": len(view["stat"]["Shares"]), 2032 "cost": view["stat"]["sharesCostRUB"], 2033 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 "Bonds": { 2036 "uniques": len(view["stat"]["Bonds"]), 2037 "cost": view["stat"]["bondsCostRUB"], 2038 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2039 }, 2040 "Etfs": { 2041 "uniques": len(view["stat"]["Etfs"]), 2042 "cost": view["stat"]["etfsCostRUB"], 2043 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2044 }, 2045 "Futures": { 2046 "uniques": len(view["stat"]["Futures"]), 2047 "cost": view["stat"]["futuresCostRUB"], 2048 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2049 }, 2050 } 2051 2052 # portfolio distribution by companies: 2053 view["analytics"]["distrByCompanies"]["All money cash"] = { 2054 "ticker": "", 2055 "cost": view["stat"]["allCurrenciesCostRUB"], 2056 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 } 2058 view["analytics"]["distrByCompanies"].update(byComp) 2059 2060 # portfolio distribution by sectors: 2061 view["analytics"]["distrBySectors"]["All money cash"] = { 2062 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2063 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2064 } 2065 view["analytics"]["distrBySectors"].update(bySect) 2066 2067 # portfolio distribution by currencies: 2068 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2069 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2070 2071 if self.moreDebug: 2072 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2073 2074 view["analytics"]["distrByCurrencies"].update(byCurr) 2075 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2076 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2077 2078 # portfolio distribution by countries: 2079 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2080 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2081 2082 if self.moreDebug: 2083 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2084 2085 view["analytics"]["distrByCountries"].update(byCountry) 2086 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2087 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2088 2089 # --- Prepare text statistics overview in human-readable: 2090 if show: 2091 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2092 2093 # Whatever the value `details`, header not changes: 2094 info = [ 2095 "# Client's portfolio\n\n", 2096 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2097 "* **Account ID:** [{}]\n".format(self.accountId), 2098 ] 2099 2100 if details in ["full", "positions", "digest"]: 2101 info.extend([ 2102 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2103 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2104 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2105 view["stat"]["totalChangesRUB"], 2106 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2107 view["stat"]["totalChangesPercentRUB"], 2108 ), 2109 ]) 2110 2111 if details in ["full", "positions"]: 2112 info.extend([ 2113 "## Open positions\n\n", 2114 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2115 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2116 "| **Ruble:** | {:>31} | | | | | |\n".format( 2117 "{:.2f} ({:.2f}) rub".format( 2118 view["stat"]["availableRUB"], 2119 view["stat"]["blockedRUB"], 2120 ) 2121 ) 2122 ]) 2123 2124 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2125 return [ 2126 "| | | | | | | |\n", 2127 "| {:<27} | | | | | {:>19} | |\n".format( 2128 noTradeStr if noTradeStr else typeStr, 2129 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2130 ), 2131 ] 2132 2133 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2134 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2135 "{} [{}]".format(data["ticker"], data["figi"]), 2136 "{:.2f} ({:.2f}) {}".format( 2137 data["volume"], 2138 data["blocked"], 2139 data["currency"], 2140 ) if isCurr else "{:.0f} ({:.0f})".format( 2141 data["volume"], 2142 data["blocked"], 2143 ), 2144 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2145 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2146 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2147 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2148 "{}{:.2f} {} ({}{:.2f}%)".format( 2149 "+" if data["profit"] > 0 else "", 2150 data["profit"], data["baseCurrencyName"], 2151 "+" if data["percentProfit"] > 0 else "", 2152 data["percentProfit"], 2153 ), 2154 ) 2155 2156 # --- Show currencies section: 2157 if view["stat"]["Currencies"]: 2158 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2159 for item in view["stat"]["Currencies"]: 2160 info.append(_InfoStr(item, isCurr=True)) 2161 2162 else: 2163 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2164 2165 # --- Show shares section: 2166 if view["stat"]["Shares"]: 2167 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2168 2169 for item in view["stat"]["Shares"]: 2170 info.append(_InfoStr(item)) 2171 2172 else: 2173 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2174 2175 # --- Show bonds section: 2176 if view["stat"]["Bonds"]: 2177 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2178 2179 for item in view["stat"]["Bonds"]: 2180 info.append(_InfoStr(item)) 2181 2182 else: 2183 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2184 2185 # --- Show etfs section: 2186 if view["stat"]["Etfs"]: 2187 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2188 2189 for item in view["stat"]["Etfs"]: 2190 info.append(_InfoStr(item)) 2191 2192 else: 2193 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2194 2195 # --- Show futures section: 2196 if view["stat"]["Futures"]: 2197 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2198 2199 for item in view["stat"]["Futures"]: 2200 info.append(_InfoStr(item)) 2201 2202 else: 2203 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2204 2205 if details in ["full", "orders"]: 2206 # --- Show pending limit orders section: 2207 if view["stat"]["orders"]: 2208 info.extend([ 2209 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2210 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2211 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2212 ]) 2213 2214 for item in view["stat"]["orders"]: 2215 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2216 "{} [{}]".format(item["ticker"], item["figi"]), 2217 item["orderID"], 2218 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2219 "{} {} ({}{:.2f}%)".format( 2220 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2221 item["baseCurrencyName"], 2222 "+" if item["percentChanges"] > 0 else "", 2223 float(item["percentChanges"]), 2224 ), 2225 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2226 item["action"], 2227 item["type"], 2228 item["date"], 2229 )) 2230 2231 else: 2232 info.append("\n## Total pending limit-orders: [0]\n") 2233 2234 # --- Show stop orders section: 2235 if view["stat"]["stopOrders"]: 2236 info.extend([ 2237 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2238 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2239 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2240 ]) 2241 2242 for item in view["stat"]["stopOrders"]: 2243 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2244 "{} [{}]".format(item["ticker"], item["figi"]), 2245 item["orderID"], 2246 item["lotsRequested"], 2247 "{} {} ({}{:.2f}%)".format( 2248 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2249 item["baseCurrencyName"], 2250 "+" if item["percentChanges"] > 0 else "", 2251 float(item["percentChanges"]), 2252 ), 2253 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2254 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2255 item["action"], 2256 item["type"], 2257 item["expType"], 2258 item["createDate"], 2259 item["expDate"], 2260 )) 2261 2262 else: 2263 info.append("\n## Total stop-orders: [0]\n") 2264 2265 if details in ["full", "analytics"]: 2266 # -- Show analytics section: 2267 if view["stat"]["portfolioCostRUB"] > 0: 2268 info.extend([ 2269 "\n# Analytics\n\n" 2270 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2271 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2272 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2273 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2274 view["stat"]["totalChangesRUB"], 2275 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2276 view["stat"]["totalChangesPercentRUB"], 2277 ), 2278 "\n## Portfolio distribution by assets\n" 2279 "\n| Type | Uniques | Percent | Current cost |\n", 2280 "|------------------------------------|---------|---------|--------------------|\n", 2281 ]) 2282 2283 for key in view["analytics"]["distrByAssets"].keys(): 2284 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2285 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2286 key, 2287 view["analytics"]["distrByAssets"][key]["uniques"], 2288 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2289 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2290 )) 2291 2292 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2293 2294 info.extend([ 2295 "\n## Portfolio distribution by companies\n" 2296 "\n| Company | Percent | Current cost |\n", 2297 aSepLine, 2298 ]) 2299 2300 for company in view["analytics"]["distrByCompanies"].keys(): 2301 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2302 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2303 "{}{}".format( 2304 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2305 company, 2306 ), 2307 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2308 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2309 )) 2310 2311 info.extend([ 2312 "\n## Portfolio distribution by sectors\n" 2313 "\n| Sector | Percent | Current cost |\n", 2314 aSepLine, 2315 ]) 2316 2317 for sector in view["analytics"]["distrBySectors"].keys(): 2318 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2319 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2320 sector, 2321 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2323 )) 2324 2325 info.extend([ 2326 "\n## Portfolio distribution by currencies\n" 2327 "\n| Instruments currencies | Percent | Current cost |\n", 2328 aSepLine, 2329 ]) 2330 2331 for curr in view["analytics"]["distrByCurrencies"].keys(): 2332 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2333 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2334 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2335 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2337 )) 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by countries\n" 2341 "\n| Assets by country | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for country in view["analytics"]["distrByCountries"].keys(): 2346 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 country, 2349 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2351 )) 2352 2353 if details in ["full", "calendar"]: 2354 # -- Show bonds payment calendar section: 2355 if view["stat"]["Bonds"]: 2356 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2357 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2358 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2359 2360 else: 2361 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2362 2363 infoText = "".join(info) 2364 2365 uLogger.info(infoText) 2366 2367 if details == "full" and self.overviewFile: 2368 filename = self.overviewFile 2369 2370 elif details == "digest" and self.overviewDigestFile: 2371 filename = self.overviewDigestFile 2372 2373 elif details == "positions" and self.overviewPositionsFile: 2374 filename = self.overviewPositionsFile 2375 2376 elif details == "orders" and self.overviewOrdersFile: 2377 filename = self.overviewOrdersFile 2378 2379 elif details == "analytics" and self.overviewAnalyticsFile: 2380 filename = self.overviewAnalyticsFile 2381 2382 elif details == "calendar" and self.overviewBondsCalendarFile: 2383 filename = self.overviewBondsCalendarFile 2384 2385 else: 2386 filename = "" 2387 2388 if filename: 2389 with open(filename, "w", encoding="UTF-8") as fH: 2390 fH.write(infoText) 2391 2392 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2393 2394 if self.useHTMLReports: 2395 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2396 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2397 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2398 2399 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2400 2401 return view 2402 2403 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2404 """ 2405 Returns history operations between two given dates for current `accountId`. 2406 If `reportFile` string is not empty then also save human-readable report. 2407 Shows some statistical data of closed positions. 2408 2409 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2410 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2411 :param show: if `True` then also prints all records to the console. 2412 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2413 :return: original list of dictionaries with history of deals records from API ("operations" key): 2414 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2415 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2416 """ 2417 if self.accountId is None or not self.accountId: 2418 uLogger.error("Variable `accountId` must be defined for using this method!") 2419 raise Exception("Account ID required") 2420 2421 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2422 2423 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2424 2425 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2426 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2427 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2428 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2429 customStat = {} # custom statistics in additional to responseJSON 2430 2431 # --- output report in human-readable format: 2432 if show or self.reportFile: 2433 splitLine1 = "| | | | | |\n" # Summary section 2434 splitLine2 = "| | | | | | | | |\n" # Operations section 2435 nextDay = "" 2436 2437 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2438 2439 if len(ops) > 0: 2440 customStat = { 2441 "opsCount": 0, # total operations count 2442 "buyCount": 0, # buy operations 2443 "sellCount": 0, # sell operations 2444 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2445 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2446 "payIn": {"rub": 0.}, # Deposit brokerage account 2447 "payOut": {"rub": 0.}, # Withdrawals 2448 "divs": {"rub": 0.}, # Dividends income 2449 "coupons": {"rub": 0.}, # Coupon's income 2450 "brokerCom": {"rub": 0.}, # Service commissions 2451 "serviceCom": {"rub": 0.}, # Service commissions 2452 "marginCom": {"rub": 0.}, # Margin commissions 2453 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2454 } 2455 2456 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2457 for item in ops: 2458 if item["state"] == "OPERATION_STATE_EXECUTED": 2459 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2460 2461 # count buy operations: 2462 if "_BUY" in item["operationType"]: 2463 customStat["buyCount"] += 1 2464 2465 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2466 customStat["buyTotal"][item["payment"]["currency"]] += payment 2467 2468 else: 2469 customStat["buyTotal"][item["payment"]["currency"]] = payment 2470 2471 # count sell operations: 2472 elif "_SELL" in item["operationType"]: 2473 customStat["sellCount"] += 1 2474 2475 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2476 customStat["sellTotal"][item["payment"]["currency"]] += payment 2477 2478 else: 2479 customStat["sellTotal"][item["payment"]["currency"]] = payment 2480 2481 # count incoming operations: 2482 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2483 if item["payment"]["currency"] in customStat["payIn"].keys(): 2484 customStat["payIn"][item["payment"]["currency"]] += payment 2485 2486 else: 2487 customStat["payIn"][item["payment"]["currency"]] = payment 2488 2489 # count withdrawals operations: 2490 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2491 if item["payment"]["currency"] in customStat["payOut"].keys(): 2492 customStat["payOut"][item["payment"]["currency"]] += payment 2493 2494 else: 2495 customStat["payOut"][item["payment"]["currency"]] = payment 2496 2497 # count dividends income: 2498 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2499 if item["payment"]["currency"] in customStat["divs"].keys(): 2500 customStat["divs"][item["payment"]["currency"]] += payment 2501 2502 else: 2503 customStat["divs"][item["payment"]["currency"]] = payment 2504 2505 # count coupon's income: 2506 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2507 if item["payment"]["currency"] in customStat["coupons"].keys(): 2508 customStat["coupons"][item["payment"]["currency"]] += payment 2509 2510 else: 2511 customStat["coupons"][item["payment"]["currency"]] = payment 2512 2513 # count broker commissions: 2514 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2515 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2516 customStat["brokerCom"][item["payment"]["currency"]] += payment 2517 2518 else: 2519 customStat["brokerCom"][item["payment"]["currency"]] = payment 2520 2521 # count service commissions: 2522 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2523 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2524 customStat["serviceCom"][item["payment"]["currency"]] += payment 2525 2526 else: 2527 customStat["serviceCom"][item["payment"]["currency"]] = payment 2528 2529 # count margin commissions: 2530 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2531 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2532 customStat["marginCom"][item["payment"]["currency"]] += payment 2533 2534 else: 2535 customStat["marginCom"][item["payment"]["currency"]] = payment 2536 2537 # count withholding taxes: 2538 elif "_TAX" in item["operationType"]: 2539 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2540 customStat["allTaxes"][item["payment"]["currency"]] += payment 2541 2542 else: 2543 customStat["allTaxes"][item["payment"]["currency"]] = payment 2544 2545 else: 2546 continue 2547 2548 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2549 2550 # --- view "Actions" lines: 2551 info.extend([ 2552 "| Report sections | | | | |\n", 2553 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2554 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2555 "| | Buy: {:<22} | {:<28} | | |\n".format( 2556 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2557 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2558 ), 2559 "| | Sell: {:<21} | {:<28} | | |\n".format( 2560 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2561 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2562 ), 2563 ]) 2564 2565 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2566 for key in opsKeys: 2567 if key == "rub": 2568 continue 2569 2570 info.extend([ 2571 "| | | {:<28} | | |\n".format( 2572 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2573 ), 2574 "| | | {:<28} | | |\n".format( 2575 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2576 ), 2577 ]) 2578 2579 info.append(splitLine1) 2580 2581 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2582 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2583 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2584 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2587 ) 2588 2589 # --- view "Payments" lines: 2590 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2591 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2592 2593 for key in paymentsKeys: 2594 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2595 2596 info.append(splitLine1) 2597 2598 # --- view "Commissions and taxes" lines: 2599 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2600 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2601 2602 for key in comKeys: 2603 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2604 2605 info.extend([ 2606 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2607 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2608 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2609 ]) 2610 2611 else: 2612 info.append("Broker returned no operations during this period\n") 2613 2614 # --- view "Operations" section: 2615 for item in ops: 2616 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2617 continue 2618 2619 else: 2620 self._figi = item["figi"] if item["figi"] else "" 2621 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2622 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2623 2624 # group of deals during one day: 2625 if nextDay and item["date"].split("T")[0] != nextDay: 2626 info.append(splitLine2) 2627 nextDay = "" 2628 2629 else: 2630 nextDay = item["date"].split("T")[0] # saving current day for splitting 2631 2632 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2633 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2634 self._figi if self._figi else "—", 2635 instrument["ticker"] if instrument else "—", 2636 instrument["type"] if instrument else "—", 2637 item["quantity"] if int(item["quantity"]) > 0 else "—", 2638 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2639 TKS_OPERATION_STATES[item["state"]], 2640 TKS_OPERATION_TYPES[item["operationType"]], 2641 )) 2642 2643 infoText = "".join(info) 2644 2645 if show: 2646 if self.moreDebug: 2647 uLogger.debug("Records about history of a client's operations successfully received") 2648 2649 uLogger.info(infoText) 2650 2651 if self.reportFile: 2652 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2653 fH.write(infoText) 2654 2655 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2656 2657 if self.useHTMLReports: 2658 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2659 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2660 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2661 2662 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2663 2664 return ops, customStat 2665 2666 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2667 """ 2668 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2669 2670 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2671 Warning! Broker server used ISO UTC time by default. 2672 2673 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2674 Also, `historyFile` used to update history with `onlyMissing` parameter. 2675 2676 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2677 2678 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2679 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2680 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2681 `"hour"`, `"day"`. Default: `"hour"`. 2682 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2683 False by default. Warning! History appends only from last candle to current time 2684 with always update last candle! 2685 :param csvSep: separator if csv-file is used, `,` by default. 2686 :param show: if `True` then also prints Pandas DataFrame to the console. 2687 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2688 `["date", "time", "open", "high", "low", "close", "volume"]`. 2689 """ 2690 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2691 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2692 history = None # empty pandas object for history 2693 2694 if interval not in TKS_CANDLE_INTERVALS.keys(): 2695 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2696 raise Exception("Incorrect value") 2697 2698 if not (self._ticker or self._figi): 2699 uLogger.error("Ticker or FIGI must be defined!") 2700 raise Exception("Ticker or FIGI required") 2701 2702 if self._ticker and not self._figi: 2703 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2704 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2705 2706 if self._figi and not self._ticker: 2707 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2708 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2709 2710 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2711 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2712 if interval.lower() != "day": 2713 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2714 2715 delta = dtEnd - dtStart # current UTC time minus last time in file 2716 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2717 2718 # calculate history length in candles: 2719 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2720 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2721 length += 1 # to avoid fraction time 2722 2723 # calculate data blocks count: 2724 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2725 2726 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2727 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2728 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2729 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2730 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2731 2732 tempOld = None # pandas object for old history, if --only-missing key present 2733 lastTime = None # datetime object of last old candle in file 2734 2735 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2736 uLogger.debug("--only-missing key present, add only last missing candles...") 2737 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2738 2739 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2740 2741 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2742 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2743 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2744 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2745 2746 # get last datetime object from last string in file or minus 1 delta if file is empty: 2747 if len(tempOld) > 0: 2748 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2749 2750 else: 2751 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2752 2753 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2754 2755 responseJSONs = [] # raw history blocks of data 2756 2757 blockEnd = dtEnd 2758 for item in range(blocks): 2759 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2760 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2761 2762 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2763 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2764 )) 2765 2766 if blockStart == blockEnd: 2767 uLogger.debug("Skipped this zero-length block...") 2768 2769 else: 2770 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2771 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2772 self.body = str({ 2773 "figi": self._figi, 2774 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2775 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2776 "interval": TKS_CANDLE_INTERVALS[interval][0] 2777 }) 2778 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2779 2780 if "code" in responseJSON.keys(): 2781 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2782 2783 else: 2784 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2785 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2786 2787 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2788 2789 blockEnd = blockStart 2790 2791 printCount = len(responseJSONs) # candles to show in console 2792 if responseJSONs: 2793 tempHistory = pd.DataFrame( 2794 data={ 2795 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2796 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2797 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2798 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2799 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2800 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2801 "volume": [int(item["volume"]) for item in responseJSONs], 2802 }, 2803 index=range(len(responseJSONs)), 2804 columns=["date", "time", "open", "high", "low", "close", "volume"], 2805 ) 2806 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2807 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2808 2809 # append only newest candles to old history if --only-missing key present: 2810 if onlyMissing and tempOld is not None and lastTime is not None: 2811 index = 0 # find start index in tempHistory data: 2812 2813 for i, item in tempHistory.iterrows(): 2814 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2815 2816 if curTime == lastTime: 2817 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2818 index = i 2819 printCount = index + 1 2820 break 2821 2822 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2823 2824 else: 2825 history = tempHistory # if no `--only-missing` key then load full data from server 2826 2827 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2828 2829 if history is not None and not history.empty: 2830 if show: 2831 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2832 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2833 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2834 )) 2835 2836 else: 2837 uLogger.warning("Received an empty candles history!") 2838 2839 if self.historyFile is not None: 2840 if history is not None and not history.empty: 2841 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2842 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2843 2844 else: 2845 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2846 2847 else: 2848 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2849 2850 return history 2851 2852 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2853 """ 2854 Load candles history from csv-file and return Pandas DataFrame object. 2855 2856 See also: `History()` and `ShowHistoryChart()` methods. 2857 2858 :param filePath: path to csv-file to open. 2859 """ 2860 loadedHistory = None # init candles data object 2861 2862 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2863 2864 if os.path.exists(filePath): 2865 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2866 2867 tfStr = self.priceModel.FormattedDelta( 2868 self.priceModel.timeframe, 2869 "{days} days {hours}h {minutes}m {seconds}s", 2870 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2871 self.priceModel.timeframe, 2872 "{hours}h {minutes}m {seconds}s", 2873 ) 2874 2875 if loadedHistory is not None and not loadedHistory.empty: 2876 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2877 len(loadedHistory), 2878 tfStr, 2879 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2880 ) 2881 2882 else: 2883 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2884 2885 else: 2886 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2887 2888 return loadedHistory 2889 2890 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2891 """ 2892 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2893 2894 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2895 Default: `index.html` (both for interact and non-interact candlesticks chart). 2896 2897 See also: `History()` and `LoadHistory()` methods. 2898 2899 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2900 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2901 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2902 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2903 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2904 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2905 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2906 """ 2907 if isinstance(candles, str): 2908 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2909 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2910 2911 elif isinstance(candles, pd.DataFrame): 2912 self.priceModel.prices = candles # set candles chain from variable 2913 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2914 2915 if "datetime" not in candles.columns: 2916 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2917 2918 else: 2919 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2920 raise Exception("Incorrect value") 2921 2922 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2923 2924 if interact: 2925 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2926 2927 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2928 2929 else: 2930 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2931 2932 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2933 2934 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2935 2936 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2937 """ 2938 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2939 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2940 2941 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2942 2943 :param operation: string "Buy" or "Sell". 2944 :param lots: volume, integer count of lots >= 1. 2945 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2946 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2947 :param expDate: string "Undefined" by default or local date in future, 2948 it is a string with format `%Y-%m-%d %H:%M:%S`. 2949 :return: JSON with response from broker server. 2950 """ 2951 if self.accountId is None or not self.accountId: 2952 uLogger.error("Variable `accountId` must be defined for using this method!") 2953 raise Exception("Account ID required") 2954 2955 if operation is None or not operation or operation not in ("Buy", "Sell"): 2956 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2957 raise Exception("Incorrect value") 2958 2959 if lots is None or lots < 1: 2960 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2961 lots = 1 2962 2963 if tp is None or tp < 0: 2964 tp = 0 2965 2966 if sl is None or sl < 0: 2967 sl = 0 2968 2969 if expDate is None or not expDate: 2970 expDate = "Undefined" 2971 2972 if not (self._ticker or self._figi): 2973 uLogger.error("Ticker or FIGI must be defined!") 2974 raise Exception("Ticker or FIGI required") 2975 2976 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2977 self._ticker = instrument["ticker"] 2978 self._figi = instrument["figi"] 2979 2980 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2981 2982 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2983 self.body = str({ 2984 "figi": self._figi, 2985 "quantity": str(lots), 2986 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2987 "accountId": str(self.accountId), 2988 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2989 }) 2990 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2991 2992 if "orderId" in response.keys(): 2993 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2994 operation, response["orderId"], 2995 self._ticker, self._figi, lots, 2996 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2997 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2998 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2999 )) 3000 3001 if tp > 0: 3002 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3003 3004 if sl > 0: 3005 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3006 3007 else: 3008 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3009 3010 return response 3011 3012 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3013 """ 3014 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3015 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3016 3017 See also: `Order()` and `Trade()` docstrings. 3018 3019 :param lots: volume, integer count of lots >= 1. 3020 :param tp: float > 0, take profit price of stop-order. 3021 :param sl: float > 0, stop loss price of stop-order. 3022 :param expDate: it's a local date in future. 3023 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3024 :return: JSON with response from broker server. 3025 """ 3026 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3027 3028 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3029 """ 3030 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3031 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3032 3033 See also: `Order()` and `Trade()` docstrings. 3034 3035 :param lots: volume, integer count of lots >= 1. 3036 :param tp: float > 0, take profit price of stop-order. 3037 :param sl: float > 0, stop loss price of stop-order. 3038 :param expDate: it's a local date in the future. 3039 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3040 :return: JSON with response from broker server. 3041 """ 3042 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3043 3044 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3045 """ 3046 Close position of given instruments. 3047 3048 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3049 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3050 This avoids unnecessary downloading data from the server. 3051 """ 3052 if instruments is None or not instruments: 3053 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3054 raise Exception("Ticker or FIGI required") 3055 3056 if isinstance(instruments, str): 3057 instruments = [instruments] 3058 3059 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3060 if uniqueInstruments: 3061 if portfolio is None or not portfolio: 3062 portfolio = self.Overview(show=False) 3063 3064 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3065 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3066 3067 for self._figi in uniqueInstruments: 3068 if self._figi not in allOpened: 3069 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3070 continue 3071 3072 # search open trade info about instrument by ticker: 3073 instrument = {} 3074 for iType in TKS_INSTRUMENTS: 3075 if instrument: 3076 break 3077 3078 for item in portfolio["stat"][iType]: 3079 if item["figi"] == self._figi: 3080 instrument = item 3081 break 3082 3083 if instrument: 3084 self._ticker = instrument["ticker"] 3085 self._figi = instrument["figi"] 3086 3087 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3088 self._ticker, 3089 self._figi, 3090 int(instrument["volume"]), 3091 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3092 )) 3093 3094 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3095 3096 if tradeLots > 0: 3097 if instrument["blocked"] > 0: 3098 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3099 instrument["blocked"], 3100 self._ticker, 3101 tradeLots, 3102 )) 3103 3104 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3105 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3106 3107 else: 3108 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3109 3110 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3111 """ 3112 Close all positions of given instruments with defined type. 3113 3114 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3115 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3116 This avoids unnecessary downloading data from the server. 3117 """ 3118 if iType not in TKS_INSTRUMENTS: 3119 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3120 3121 else: 3122 if portfolio is None or not portfolio: 3123 portfolio = self.Overview(show=False) 3124 3125 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3126 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3127 3128 if tickers and portfolio: 3129 self.CloseTrades(tickers, portfolio) 3130 3131 else: 3132 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3133 3134 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3135 """ 3136 Universal method to create market or limit orders with all available parameters for current `accountId`. 3137 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3138 3139 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3140 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3141 3142 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3143 then broker immediately open market order as you can do simple --buy or --sell operations! 3144 3145 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3146 When current price will go up or down to target price value then broker opens a limit order. 3147 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3148 3149 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3150 3151 :param operation: string "Buy" or "Sell". 3152 :param orderType: string "Limit" or "Stop". 3153 :param lots: volume, integer count of lots >= 1. 3154 :param targetPrice: target price > 0. This is open trade price for limit order. 3155 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3156 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3157 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3158 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3159 Stop loss order always executed by market price. 3160 :param expDate: string "Undefined" by default or local date in future. 3161 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3162 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3163 A limit order has no expiration date, it lasts until the end of the trading day. 3164 :return: JSON with response from broker server. 3165 """ 3166 if self.accountId is None or not self.accountId: 3167 uLogger.error("Variable `accountId` must be defined for using this method!") 3168 raise Exception("Account ID required") 3169 3170 if operation is None or not operation or operation not in ("Buy", "Sell"): 3171 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3172 raise Exception("Incorrect value") 3173 3174 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3175 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3176 raise Exception("Incorrect value") 3177 3178 if lots is None or lots < 1: 3179 uLogger.error("You must define trade volume > 0: integer count of lots!") 3180 raise Exception("Incorrect value") 3181 3182 if targetPrice is None or targetPrice <= 0: 3183 uLogger.error("Target price for limit-order must be greater than 0!") 3184 raise Exception("Incorrect value") 3185 3186 if limitPrice is None or limitPrice <= 0: 3187 limitPrice = targetPrice 3188 3189 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3190 stopType = "Limit" 3191 3192 if expDate is None or not expDate: 3193 expDate = "Undefined" 3194 3195 if not (self._ticker or self._figi): 3196 uLogger.error("Tocker or FIGI must be defined!") 3197 raise Exception("Ticker or FIGI required") 3198 3199 response = {} 3200 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3201 self._ticker = instrument["ticker"] 3202 self._figi = instrument["figi"] 3203 3204 if orderType == "Limit": 3205 uLogger.debug( 3206 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3207 self._ticker, self._figi, 3208 operation, lots, targetPrice, instrument["currency"], 3209 )) 3210 3211 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3212 self.body = str({ 3213 "figi": self._figi, 3214 "quantity": str(lots), 3215 "price": FloatToNano(targetPrice), 3216 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3217 "accountId": str(self.accountId), 3218 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3219 }) 3220 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3221 3222 if "orderId" in response.keys(): 3223 uLogger.info( 3224 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3225 response["orderId"], self._ticker, self._figi, operation, lots, 3226 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3227 )) 3228 3229 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3230 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3231 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3232 targetPrice, instrument["currency"], 3233 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3234 )) 3235 3236 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3237 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3238 targetPrice, instrument["currency"], 3239 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3240 )) 3241 3242 else: 3243 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3244 3245 if orderType == "Stop": 3246 uLogger.debug( 3247 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3248 self._ticker, self._figi, 3249 operation, lots, 3250 targetPrice, instrument["currency"], 3251 limitPrice, instrument["currency"], 3252 stopType, expDate, 3253 )) 3254 3255 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3256 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3257 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3258 3259 body = { 3260 "figi": self._figi, 3261 "quantity": str(lots), 3262 "price": FloatToNano(limitPrice), 3263 "stopPrice": FloatToNano(targetPrice), 3264 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3265 "accountId": str(self.accountId), 3266 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3267 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3268 } 3269 3270 if expDateUTC: 3271 body["expireDate"] = expDateUTC 3272 3273 self.body = str(body) 3274 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3275 3276 if "stopOrderId" in response.keys(): 3277 uLogger.info( 3278 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3279 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3280 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3281 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3282 TKS_STOP_ORDER_TYPES[stopOrderType], 3283 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3284 )) 3285 3286 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3287 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3288 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3289 targetPrice, instrument["currency"], 3290 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3291 )) 3292 3293 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3294 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3295 targetPrice, instrument["currency"], 3296 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3297 )) 3298 3299 else: 3300 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3301 3302 return response 3303 3304 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3305 """ 3306 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3307 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3308 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3309 See also: `Order()` docstring. 3310 3311 :param lots: volume, integer count of lots >= 1. 3312 :param targetPrice: target price > 0. This is open trade price for limit order. 3313 :return: JSON with response from broker server. 3314 """ 3315 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3316 3317 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3318 """ 3319 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3320 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3321 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3322 target price value then broker opens a limit order. See also: `Order()` docstring. 3323 3324 :param lots: volume, integer count of lots >= 1. 3325 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3326 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3327 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3328 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3329 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3330 :param expDate: string "Undefined" by default or local date in future. 3331 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3332 This date is converting to UTC format for server. 3333 :return: JSON with response from broker server. 3334 """ 3335 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3336 3337 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3338 """ 3339 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3340 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3341 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3342 See also: `Order()` docstring. 3343 3344 :param lots: volume, integer count of lots >= 1. 3345 :param targetPrice: target price > 0. This is open trade price for limit order. 3346 :return: JSON with response from broker server. 3347 """ 3348 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3349 3350 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3351 """ 3352 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3353 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3354 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3355 target price value then broker opens a limit order. See also: `Order()` docstring. 3356 3357 :param lots: volume, integer count of lots >= 1. 3358 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3359 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3360 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3361 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3362 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3363 :param expDate: string "Undefined" by default or local date in future. 3364 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3365 This date is converting to UTC format for server. 3366 :return: JSON with response from broker server. 3367 """ 3368 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3369 3370 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3371 """ 3372 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3373 3374 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3375 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3376 This avoids unnecessary downloading data from the server. 3377 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3378 """ 3379 if self.accountId is None or not self.accountId: 3380 uLogger.error("Variable `accountId` must be defined for using this method!") 3381 raise Exception("Account ID required") 3382 3383 if orderIDs: 3384 if allOrdersIDs is None: 3385 rawOrders = self.RequestPendingOrders() 3386 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3387 3388 if allStopOrdersIDs is None: 3389 rawStopOrders = self.RequestStopOrders() 3390 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3391 3392 for orderID in orderIDs: 3393 idInPendingOrders = orderID in allOrdersIDs 3394 idInStopOrders = orderID in allStopOrdersIDs 3395 3396 if not (idInPendingOrders or idInStopOrders): 3397 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3398 continue 3399 3400 else: 3401 if idInPendingOrders: 3402 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3403 3404 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3405 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3406 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3407 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3408 3409 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3410 if self.moreDebug: 3411 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3412 3413 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3414 3415 else: 3416 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3417 3418 elif idInStopOrders: 3419 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3420 3421 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3422 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3423 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3424 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3425 3426 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3427 if self.moreDebug: 3428 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3429 3430 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3431 3432 else: 3433 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3434 3435 else: 3436 continue 3437 3438 def CloseAllOrders(self) -> None: 3439 """ 3440 Gets a list of open pending and stop orders and cancel it all. 3441 """ 3442 rawOrders = self.RequestPendingOrders() 3443 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3444 lenOrders = len(allOrdersIDs) 3445 3446 rawStopOrders = self.RequestStopOrders() 3447 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3448 lenSOrders = len(allStopOrdersIDs) 3449 3450 if lenOrders > 0 or lenSOrders > 0: 3451 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3452 3453 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3454 3455 else: 3456 uLogger.info("Orders not found, nothing to cancel.") 3457 3458 def CloseAll(self, *args) -> None: 3459 """ 3460 Close all available (not blocked) opened trades and orders. 3461 3462 Also, you can select one or more keywords case-insensitive: 3463 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3464 3465 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3466 """ 3467 overview = self.Overview(show=False) # get all open trades info 3468 3469 if len(args) == 0: 3470 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3471 self.CloseAllOrders() # close all pending and stop orders 3472 3473 for iType in TKS_INSTRUMENTS: 3474 if iType != "Currencies": 3475 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3476 3477 else: 3478 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3479 lowerArgs = [x.lower() for x in args] 3480 3481 if "orders" in lowerArgs: 3482 self.CloseAllOrders() # close all pending and stop orders 3483 3484 for iType in TKS_INSTRUMENTS: 3485 if iType.lower() in lowerArgs and iType != "Currencies": 3486 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3487 3488 def CloseAllByTicker(self, instrument: str) -> None: 3489 """ 3490 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3491 3492 This method searches opened trade and orders of instrument throw all portfolio and then use 3493 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3494 3495 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3496 3497 :param instrument: string with ticker. 3498 """ 3499 if instrument is None or not instrument: 3500 uLogger.error("Ticker name must be defined for using this method!") 3501 raise Exception("Ticker required") 3502 3503 overview = self.Overview(show=False) # get user portfolio with all open trades info 3504 3505 self._ticker = instrument # try to set instrument as ticker 3506 self._figi = "" 3507 3508 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3509 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3510 3511 if limitAll and self.IsInLimitOrders(portfolio=overview): 3512 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3513 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3514 3515 if stopAll and self.IsInStopOrders(portfolio=overview): 3516 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3517 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3518 3519 if self.IsInPortfolio(portfolio=overview): 3520 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3521 self.CloseTrades(instruments=[instrument], portfolio=overview) 3522 3523 def CloseAllByFIGI(self, instrument: str) -> None: 3524 """ 3525 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3526 3527 This method searches opened trade and orders of instrument throw all portfolio and then use 3528 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3529 3530 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3531 3532 :param instrument: string with FIGI id. 3533 """ 3534 if instrument is None or not instrument: 3535 uLogger.error("FIGI id must be defined for using this method!") 3536 raise Exception("FIGI required") 3537 3538 overview = self.Overview(show=False) # get user portfolio with all open trades info 3539 3540 self._ticker = "" 3541 self._figi = instrument # try to set instrument as FIGI id 3542 3543 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3544 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3545 3546 if limitAll and self.IsInLimitOrders(portfolio=overview): 3547 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3548 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3549 3550 if stopAll and self.IsInStopOrders(portfolio=overview): 3551 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3552 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3553 3554 if self.IsInPortfolio(portfolio=overview): 3555 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3556 self.CloseTrades(instruments=[instrument], portfolio=overview) 3557 3558 @staticmethod 3559 def ParseOrderParameters(operation, **inputParameters): 3560 """ 3561 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3562 3563 :param operation: string "Buy" or "Sell". 3564 :param inputParameters: this is dict of strings that looks like this 3565 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3566 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3567 "prices" key: one or more prices to open limit-orders 3568 Counts of values in lots and prices lists must be equals! 3569 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3570 """ 3571 # TODO: update order grid work with api v2 3572 pass 3573 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3574 # 3575 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3576 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3577 # raise Exception("Incorrect value") 3578 # 3579 # if "l" in inputParameters.keys(): 3580 # inputParameters["lots"] = inputParameters.pop("l") 3581 # 3582 # if "p" in inputParameters.keys(): 3583 # inputParameters["prices"] = inputParameters.pop("p") 3584 # 3585 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3586 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3587 # raise Exception("Incorrect value") 3588 # 3589 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3590 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3591 # 3592 # if len(lots) != len(prices): 3593 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3594 # raise Exception("Incorrect value") 3595 # 3596 # uLogger.debug("Extracted parameters for orders:") 3597 # uLogger.debug("lots = {}".format(lots)) 3598 # uLogger.debug("prices = {}".format(prices)) 3599 # 3600 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3601 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3602 # uLogger.debug("Order parameters: {}".format(result)) 3603 # 3604 # return result 3605 3606 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3607 """ 3608 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3609 3610 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3611 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3612 """ 3613 result = False 3614 msg = "Instrument not defined!" 3615 3616 if portfolio is None or not portfolio: 3617 portfolio = self.Overview(show=False) 3618 3619 if self._ticker: 3620 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3621 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3622 3623 for iType in TKS_INSTRUMENTS: 3624 for instrument in portfolio["stat"][iType]: 3625 if instrument["ticker"] == self._ticker: 3626 result = True 3627 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3628 break 3629 3630 elif self._figi: 3631 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3632 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3633 3634 for iType in TKS_INSTRUMENTS: 3635 for instrument in portfolio["stat"][iType]: 3636 if instrument["figi"] == self._figi: 3637 result = True 3638 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3639 break 3640 3641 else: 3642 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3643 3644 uLogger.debug(msg) 3645 3646 return result 3647 3648 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3649 """ 3650 Returns instrument from the user's portfolio if it presents there. 3651 Instrument must be defined by `ticker` (highly priority) or `figi`. 3652 3653 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3654 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3655 """ 3656 result = None 3657 msg = "Instrument not defined!" 3658 3659 if portfolio is None or not portfolio: 3660 portfolio = self.Overview(show=False) 3661 3662 if self._ticker: 3663 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3664 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3665 3666 for iType in TKS_INSTRUMENTS: 3667 for instrument in portfolio["stat"][iType]: 3668 if instrument["ticker"] == self._ticker: 3669 result = instrument 3670 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3671 break 3672 3673 elif self._figi: 3674 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3675 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3676 3677 for iType in TKS_INSTRUMENTS: 3678 for instrument in portfolio["stat"][iType]: 3679 if instrument["figi"] == self._figi: 3680 result = instrument 3681 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3682 break 3683 3684 else: 3685 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3686 3687 uLogger.debug(msg) 3688 3689 return result 3690 3691 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3692 """ 3693 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3694 3695 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3696 3697 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3698 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3699 """ 3700 result = False 3701 msg = "Instrument not defined!" 3702 3703 if portfolio is None or not portfolio: 3704 portfolio = self.Overview(show=False) 3705 3706 if self._ticker: 3707 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3708 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3709 3710 for instrument in portfolio["stat"]["orders"]: 3711 if instrument["ticker"] == self._ticker: 3712 result = True 3713 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3714 break 3715 3716 elif self._figi: 3717 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3718 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3719 3720 for instrument in portfolio["stat"]["orders"]: 3721 if instrument["figi"] == self._figi: 3722 result = True 3723 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3724 break 3725 3726 else: 3727 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3728 3729 uLogger.debug(msg) 3730 3731 return result 3732 3733 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3734 """ 3735 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3736 Instrument must be defined by `ticker` (highly priority) or `figi`. 3737 3738 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3739 3740 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3741 :return: list with `orderID`s of limit orders. 3742 """ 3743 result = [] 3744 msg = "Instrument not defined!" 3745 3746 if portfolio is None or not portfolio: 3747 portfolio = self.Overview(show=False) 3748 3749 if self._ticker: 3750 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3751 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3752 3753 for instrument in portfolio["stat"]["orders"]: 3754 if instrument["ticker"] == self._ticker: 3755 result.append(instrument["orderID"]) 3756 3757 if result: 3758 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3759 3760 elif self._figi: 3761 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3762 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3763 3764 for instrument in portfolio["stat"]["orders"]: 3765 if instrument["figi"] == self._figi: 3766 result.append(instrument["orderID"]) 3767 3768 if result: 3769 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3770 3771 else: 3772 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3773 3774 uLogger.debug(msg) 3775 3776 return result 3777 3778 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3779 """ 3780 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3781 3782 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3783 3784 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3785 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3786 """ 3787 result = False 3788 msg = "Instrument not defined!" 3789 3790 if portfolio is None or not portfolio: 3791 portfolio = self.Overview(show=False) 3792 3793 if self._ticker: 3794 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3795 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3796 3797 for instrument in portfolio["stat"]["stopOrders"]: 3798 if instrument["ticker"] == self._ticker: 3799 result = True 3800 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3801 break 3802 3803 elif self._figi: 3804 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3805 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3806 3807 for instrument in portfolio["stat"]["stopOrders"]: 3808 if instrument["figi"] == self._figi: 3809 result = True 3810 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3811 break 3812 3813 else: 3814 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3815 3816 uLogger.debug(msg) 3817 3818 return result 3819 3820 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3821 """ 3822 Returns list with all `orderID`s of opened stop orders for the instrument. 3823 Instrument must be defined by `ticker` (highly priority) or `figi`. 3824 3825 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3826 3827 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3828 :return: list with `orderID`s of stop orders. 3829 """ 3830 result = [] 3831 msg = "Instrument not defined!" 3832 3833 if portfolio is None or not portfolio: 3834 portfolio = self.Overview(show=False) 3835 3836 if self._ticker: 3837 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3838 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3839 3840 for instrument in portfolio["stat"]["stopOrders"]: 3841 if instrument["ticker"] == self._ticker: 3842 result.append(instrument["orderID"]) 3843 3844 if result: 3845 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3846 3847 elif self._figi: 3848 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3849 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3850 3851 for instrument in portfolio["stat"]["stopOrders"]: 3852 if instrument["figi"] == self._figi: 3853 result.append(instrument["orderID"]) 3854 3855 if result: 3856 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3857 3858 else: 3859 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3860 3861 uLogger.debug(msg) 3862 3863 return result 3864 3865 def RequestLimits(self) -> dict: 3866 """ 3867 Method for obtaining the available funds for withdrawal for current `accountId`. 3868 3869 See also: 3870 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3871 - `OverviewLimits()` method 3872 3873 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3874 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3875 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3876 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3877 """ 3878 if self.accountId is None or not self.accountId: 3879 uLogger.error("Variable `accountId` must be defined for using this method!") 3880 raise Exception("Account ID required") 3881 3882 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3883 3884 self.body = str({"accountId": self.accountId}) 3885 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3886 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3887 3888 if self.moreDebug: 3889 uLogger.debug("Records about available funds for withdrawal successfully received") 3890 3891 return rawLimits 3892 3893 def OverviewLimits(self, show: bool = False) -> dict: 3894 """ 3895 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3896 3897 See also: `RequestLimits()`. 3898 3899 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3900 :return: dict with raw parsed data from server and some calculated statistics about it. 3901 """ 3902 if self.accountId is None or not self.accountId: 3903 uLogger.error("Variable `accountId` must be defined for using this method!") 3904 raise Exception("Account ID required") 3905 3906 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3907 3908 view = { 3909 "rawLimits": rawLimits, 3910 "limits": { # parsed data for every currency: 3911 "money": { # this is an array of portfolio currency positions 3912 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3913 }, 3914 "blocked": { # this is an array of blocked currency 3915 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3916 }, 3917 "blockedGuarantee": { # this is locked money under collateral for futures 3918 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3919 }, 3920 }, 3921 } 3922 3923 # --- Prepare text table with limits in human-readable format: 3924 if show: 3925 info = [ 3926 "# Withdrawal limits\n\n", 3927 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3928 "* **Account ID:** [{}]\n".format(self.accountId), 3929 ] 3930 3931 if view["limits"]["money"]: 3932 info.extend([ 3933 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3934 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3935 ]) 3936 3937 else: 3938 info.append("\nNo withdrawal limits\n") 3939 3940 for curr in view["limits"]["money"].keys(): 3941 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3942 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3943 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3944 3945 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3946 "[{}]".format(curr), 3947 "{:.2f}".format(view["limits"]["money"][curr]), 3948 "{:.2f}".format(availableMoney), 3949 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3950 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3951 ) 3952 3953 if curr == "rub": 3954 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3955 3956 else: 3957 info.append(infoStr) 3958 3959 infoText = "".join(info) 3960 3961 uLogger.info(infoText) 3962 3963 if self.withdrawalLimitsFile: 3964 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3965 fH.write(infoText) 3966 3967 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3968 3969 if self.useHTMLReports: 3970 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3971 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3972 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3973 3974 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3975 3976 return view 3977 3978 def RequestAccounts(self) -> dict: 3979 """ 3980 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3981 3982 See also: 3983 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3984 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3985 - `OverviewUserInfo()` method 3986 3987 :return: dict with raw data from server that contains accounts info. Example of dict: 3988 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3989 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3990 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3991 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3992 """ 3993 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3994 3995 self.body = str({}) 3996 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3997 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3998 3999 if self.moreDebug: 4000 uLogger.debug("Records about available accounts successfully received") 4001 4002 return rawAccounts 4003 4004 def RequestUserInfo(self) -> dict: 4005 """ 4006 Method for requesting common user's information. 4007 4008 See also: 4009 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4010 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4011 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4012 - `OverviewUserInfo()` method 4013 4014 :return: dict with raw data from server that contains user's information. Example of dict: 4015 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4016 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4017 """ 4018 uLogger.debug("Requesting common user's information. Wait, please...") 4019 4020 self.body = str({}) 4021 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4022 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4023 4024 if self.moreDebug: 4025 uLogger.debug("Records about current user successfully received") 4026 4027 return rawUserInfo 4028 4029 def RequestMarginStatus(self, accountId: str = None) -> dict: 4030 """ 4031 Method for requesting margin calculation for defined account ID. 4032 4033 See also: 4034 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4035 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4036 - `OverviewUserInfo()` method 4037 4038 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4039 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4040 Example of responses: 4041 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4042 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4043 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4044 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4045 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4046 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4047 """ 4048 if accountId is None or not accountId: 4049 if self.accountId is None or not self.accountId: 4050 uLogger.error("Variable `accountId` must be defined for using this method!") 4051 raise Exception("Account ID required") 4052 4053 else: 4054 accountId = self.accountId # use `self.accountId` (main ID) by default 4055 4056 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4057 4058 self.body = str({"accountId": accountId}) 4059 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4060 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4061 4062 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4063 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4064 rawMargin = {} 4065 4066 else: 4067 if self.moreDebug: 4068 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4069 4070 return rawMargin 4071 4072 def RequestTariffLimits(self) -> dict: 4073 """ 4074 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4075 4076 See also: 4077 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4078 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4079 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4080 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4081 - `OverviewUserInfo()` method 4082 4083 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4084 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4085 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4086 """ 4087 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4088 4089 self.body = str({}) 4090 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4091 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4092 4093 if self.moreDebug: 4094 uLogger.debug("Records with limits of current tariff successfully received") 4095 4096 return rawTariffLimits 4097 4098 def RequestBondCoupons(self, iJSON: dict) -> dict: 4099 """ 4100 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4101 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4102 All dates are in UTC timezone. 4103 4104 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4105 Documentation: 4106 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4107 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4108 4109 See also: `ExtendBondsData()`. 4110 4111 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4112 If raw iJSON is not data of bond then server returns an error [400] with message: 4113 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4114 :return: dictionary with bond payment calendar. Response example 4115 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4116 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4117 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4118 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4119 """ 4120 if iJSON["figi"] is None or not iJSON["figi"]: 4121 uLogger.error("FIGI must be defined for using this method!") 4122 raise Exception("FIGI required") 4123 4124 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4125 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4126 4127 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4128 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4129 self._figi, 4130 startDate, 4131 endDate, 4132 )) 4133 4134 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4135 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4136 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4137 4138 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4139 uLogger.warning("Instrument type is not bond!") 4140 4141 else: 4142 if self.moreDebug: 4143 uLogger.debug("Records about bond payment calendar successfully received") 4144 4145 return calendar 4146 4147 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4148 """ 4149 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4150 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4151 coupon yields, current yields and some statistics etc. 4152 4153 WARNING! This is too long operation if a lot of bonds requested from broker server. 4154 4155 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4156 4157 :param instruments: list of strings with tickers or FIGIs. 4158 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4159 for further used by data scientists or stock analytics. 4160 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4161 In XLSX-file and Pandas DataFrame fields mean: 4162 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4163 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4164 """ 4165 if instruments is None or not instruments: 4166 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4167 raise Exception("Ticker or FIGI required") 4168 4169 if isinstance(instruments, str): 4170 instruments = [instruments] 4171 4172 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4173 4174 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4175 4176 iCount = len(uniqueInstruments) 4177 tooLong = iCount >= 20 4178 if tooLong: 4179 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4180 4181 bonds = None 4182 for i, self._figi in enumerate(uniqueInstruments): 4183 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4184 4185 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4186 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4187 rawBond = self.SearchByFIGI(requestPrice=True) 4188 4189 # Widen raw data with UTC current time (iData["actualDateTime"]): 4190 actualDate = datetime.now(tzutc()) 4191 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4192 4193 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4194 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4195 4196 # Replace some values with human-readable: 4197 iData["nominalCurrency"] = iData["nominal"]["currency"] 4198 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4199 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4200 iData["aciCurrency"] = iData["aciValue"]["currency"] 4201 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4202 iData["issueSize"] = int(iData["issueSize"]) 4203 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4204 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4205 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4206 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4207 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4208 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4209 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4210 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4211 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4212 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4213 4214 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4215 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4216 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4217 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4218 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4219 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4220 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4221 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4222 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4223 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4224 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4225 4226 # Widen raw data with calendar data from `rawCalendar` values: 4227 calendarData = [] 4228 if "events" in iData["rawCalendar"].keys(): 4229 for item in iData["rawCalendar"]["events"]: 4230 calendarData.append({ 4231 "couponDate": item["couponDate"], 4232 "couponNumber": int(item["couponNumber"]), 4233 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4234 "payCurrency": item["payOneBond"]["currency"], 4235 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4236 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4237 "couponStartDate": item["couponStartDate"], 4238 "couponEndDate": item["couponEndDate"], 4239 "couponPeriod": item["couponPeriod"], 4240 }) 4241 4242 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4243 if "maturityDate" not in iData.keys(): 4244 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4245 4246 # Widen raw data with Coupon Rate. 4247 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4248 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4249 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4250 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4251 4252 # Widen raw data with Yield to Maturity (YTM) on current date. 4253 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4254 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4255 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4256 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4257 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4258 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4259 4260 iData["calendar"] = calendarData # adds calendar at the end 4261 4262 # Remove not used data: 4263 iData.pop("uid") 4264 iData.pop("positionUid") 4265 iData.pop("currentPrice") 4266 iData.pop("rawCalendar") 4267 4268 colNames = list(iData.keys()) 4269 if bonds is None: 4270 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4271 4272 else: 4273 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4274 4275 else: 4276 uLogger.warning("Instrument is not a bond!") 4277 4278 processed = round(100 * (i + 1) / iCount, 1) 4279 if tooLong and processed % 5 == 0: 4280 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4281 4282 else: 4283 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4284 4285 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4286 4287 # Saving bonds from Pandas DataFrame to XLSX sheet: 4288 if xlsx and self.bondsXLSXFile: 4289 with pd.ExcelWriter( 4290 path=self.bondsXLSXFile, 4291 date_format=TKS_DATE_FORMAT, 4292 datetime_format=TKS_DATE_TIME_FORMAT, 4293 mode="w", 4294 ) as writer: 4295 bonds.to_excel( 4296 writer, 4297 sheet_name="Extended bonds data", 4298 index=True, 4299 encoding="UTF-8", 4300 freeze_panes=(1, 1), 4301 ) # saving as XLSX-file with freeze first row and column as headers 4302 4303 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4304 4305 return bonds 4306 4307 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4308 """ 4309 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4310 4311 WARNING! This is too long operation if a lot of bonds requested from broker server. 4312 4313 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4314 4315 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4316 extended information about bonds: main info, current prices, bond payment calendar, 4317 coupon yields, current yields and some statistics etc. 4318 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4319 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4320 for further used by data scientists or stock analytics. 4321 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4322 """ 4323 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4324 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4325 4326 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4327 4328 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4329 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4330 calendar = None 4331 for bond in extBonds.iterrows(): 4332 for item in bond[1]["calendar"]: 4333 cData = { 4334 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4335 "couponDate": item["couponDate"], 4336 "figi": bond[1]["figi"], 4337 "ticker": bond[1]["ticker"], 4338 "name": bond[1]["name"], 4339 "couponNumber": item["couponNumber"], 4340 "payOneBond": item["payOneBond"], 4341 "payCurrency": item["payCurrency"], 4342 "couponType": item["couponType"], 4343 "couponPeriod": item["couponPeriod"], 4344 "fixDate": item["fixDate"], 4345 "couponStartDate": item["couponStartDate"], 4346 "couponEndDate": item["couponEndDate"], 4347 } 4348 4349 if calendar is None: 4350 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4351 4352 else: 4353 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4354 4355 if calendar is not None: 4356 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4357 4358 # Saving calendar from Pandas DataFrame to XLSX sheet: 4359 if xlsx: 4360 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4361 4362 with pd.ExcelWriter( 4363 path=xlsxCalendarFile, 4364 date_format=TKS_DATE_FORMAT, 4365 datetime_format=TKS_DATE_TIME_FORMAT, 4366 mode="w", 4367 ) as writer: 4368 humanReadable = calendar.copy(deep=True) 4369 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4370 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4371 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4372 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4373 humanReadable.columns = colNames # human-readable column names 4374 4375 humanReadable.to_excel( 4376 writer, 4377 sheet_name="Bond payments calendar", 4378 index=False, 4379 encoding="UTF-8", 4380 freeze_panes=(1, 2), 4381 ) # saving as XLSX-file with freeze first row and column as headers 4382 4383 del humanReadable # release df in memory 4384 4385 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4386 4387 return calendar 4388 4389 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4390 """ 4391 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4392 Also, creates Markdown file with calendar data, `calendar.md` by default. 4393 4394 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4395 4396 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4397 extended information about bonds: main info, current prices, bond payment calendar, 4398 coupon yields, current yields and some statistics etc. 4399 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4400 :param show: if `True` then also printing bonds payment calendar to the console, 4401 otherwise save to file `calendarFile` only. `False` by default. 4402 :return: multilines text in Markdown format with bonds payment calendar as a table. 4403 """ 4404 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4405 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4406 4407 infoText = "# Bond payments calendar\n\n" 4408 4409 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4410 4411 if not (calendar is None or calendar.empty): 4412 splitLine = "| | | | | | | | | |\n" 4413 4414 info = [ 4415 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4416 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4417 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4418 ] 4419 4420 newMonth = False 4421 notOneBond = calendar["figi"].nunique() > 1 4422 for i, bond in enumerate(calendar.iterrows()): 4423 if newMonth and notOneBond: 4424 info.append(splitLine) 4425 4426 info.append( 4427 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4428 " √" if bond[1]["paid"] else " —", 4429 bond[1]["couponDate"].split("T")[0], 4430 bond[1]["figi"], 4431 bond[1]["ticker"], 4432 bond[1]["couponNumber"], 4433 "{} {}".format( 4434 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4435 bond[1]["payCurrency"], 4436 ), 4437 bond[1]["couponType"], 4438 bond[1]["couponPeriod"], 4439 bond[1]["fixDate"].split("T")[0], 4440 ) 4441 ) 4442 4443 if i < len(calendar.values) - 1: 4444 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4445 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4446 newMonth = False if curDate.month == nextDate.month else True 4447 4448 else: 4449 newMonth = False 4450 4451 infoText += "".join(info) 4452 4453 if show: 4454 uLogger.info("{}".format(infoText)) 4455 4456 if self.calendarFile is not None: 4457 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4458 fH.write(infoText) 4459 4460 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4461 4462 if self.useHTMLReports: 4463 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4464 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4465 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4466 4467 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4468 4469 else: 4470 infoText += "No data\n" 4471 4472 return infoText 4473 4474 def OverviewAccounts(self, show: bool = False) -> dict: 4475 """ 4476 Method for parsing and show simple table with all available user accounts. 4477 4478 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4479 4480 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4481 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4482 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4483 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4484 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4485 "closed": "—", "access": "Full access" }, ...}}` 4486 """ 4487 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4488 4489 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4490 accounts = { 4491 item["id"]: { 4492 "type": TKS_ACCOUNT_TYPES[item["type"]], 4493 "name": item["name"], 4494 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4495 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4496 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4497 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4498 } for item in rawAccounts["accounts"] 4499 } 4500 4501 # Raw and parsed data with some fields replaced in "stat" section: 4502 view = { 4503 "rawAccounts": rawAccounts, 4504 "stat": accounts, 4505 } 4506 4507 # --- Prepare simple text table with only accounts data in human-readable format: 4508 if show: 4509 info = [ 4510 "# User accounts\n\n", 4511 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4512 "| Account ID | Type | Status | Name |\n", 4513 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4514 ] 4515 4516 for account in view["stat"].keys(): 4517 info.extend([ 4518 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4519 account, 4520 view["stat"][account]["type"], 4521 view["stat"][account]["status"], 4522 view["stat"][account]["name"], 4523 ) 4524 ]) 4525 4526 infoText = "".join(info) 4527 4528 uLogger.info(infoText) 4529 4530 if self.userAccountsFile: 4531 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4532 fH.write(infoText) 4533 4534 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4535 4536 if self.useHTMLReports: 4537 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4538 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4539 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4540 4541 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4542 4543 return view 4544 4545 def OverviewUserInfo(self, show: bool = False) -> dict: 4546 """ 4547 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4548 4549 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4550 4551 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4552 :return: dict with raw parsed data from server and some calculated statistics about it. 4553 """ 4554 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4555 tmpTicker = self._ticker 4556 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4557 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4558 self._ticker = tmpTicker 4559 4560 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4561 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4562 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4563 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4564 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4565 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4566 4567 # This is dict with parsed common user data: 4568 userInfo = { 4569 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4570 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4571 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4572 "tariff": rawUserInfo["tariff"], 4573 } 4574 4575 # This is an array of dict with parsed margin statuses for every account IDs: 4576 margins = {} 4577 for accountId in accounts.keys(): 4578 if rawMargins[accountId]: 4579 margins[accountId] = { 4580 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4581 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4582 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4583 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4584 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4585 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4586 "missing": missing["volume"], 4587 } 4588 4589 else: 4590 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4591 4592 unary = {} # unary-connection limits 4593 for item in rawTariffLimits["unaryLimits"]: 4594 if item["limitPerMinute"] in unary.keys(): 4595 unary[item["limitPerMinute"]].extend(item["methods"]) 4596 4597 else: 4598 unary[item["limitPerMinute"]] = item["methods"] 4599 4600 stream = {} # stream-connection limits 4601 for item in rawTariffLimits["streamLimits"]: 4602 if item["limit"] in stream.keys(): 4603 stream[item["limit"]].extend(item["streams"]) 4604 4605 else: 4606 stream[item["limit"]] = item["streams"] 4607 4608 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4609 limits = { 4610 "unary": unary, 4611 "stream": stream, 4612 } 4613 4614 # Raw and parsed data as an output result: 4615 view = { 4616 "rawUserInfo": rawUserInfo, 4617 "rawAccounts": rawAccounts, 4618 "rawMargins": rawMargins, 4619 "rawTariffLimits": rawTariffLimits, 4620 "stat": { 4621 "overview": overview, 4622 "userInfo": userInfo, 4623 "accounts": accounts, 4624 "margins": margins, 4625 "limits": limits, 4626 }, 4627 } 4628 4629 # --- Prepare text table with user information in human-readable format: 4630 if show: 4631 info = [ 4632 "# Full user information\n\n", 4633 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4634 "## Common information\n\n", 4635 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4636 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4637 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4638 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4639 "\n## User accounts\n\n", 4640 ] 4641 4642 for account in view["stat"]["accounts"].keys(): 4643 info.extend([ 4644 "### ID: [{}]\n\n".format(account), 4645 "| Parameters | Values |\n", 4646 "|----------------------|--------------------------------------------------------------|\n", 4647 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4648 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4649 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4650 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4651 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4652 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4653 ]) 4654 4655 if margins[account]: 4656 info.extend([ 4657 "| Margin status: | Enabled |\n", 4658 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4659 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4660 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4661 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4662 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4663 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4664 ]) 4665 4666 else: 4667 info.append("| Margin status: | Disabled |\n\n") 4668 4669 info.extend([ 4670 "\n## Current user tariff limits\n", 4671 "\n### See also\n", 4672 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4673 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4674 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4675 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4676 "\n### Unary limits\n", 4677 ]) 4678 4679 if unary: 4680 for key, values in sorted(unary.items()): 4681 info.append("\n* Max requests per minute: {}\n".format(key)) 4682 4683 for value in values: 4684 info.append(" - {}\n".format(value)) 4685 4686 else: 4687 info.append("\nNot available\n") 4688 4689 info.append("\n### Stream limits\n") 4690 4691 if stream: 4692 for key, values in sorted(stream.items()): 4693 info.append("\n* Max stream connections: {}\n".format(key)) 4694 4695 for value in values: 4696 info.append(" - {}\n".format(value)) 4697 4698 else: 4699 info.append("\nNot available\n") 4700 4701 infoText = "".join(info) 4702 4703 uLogger.info(infoText) 4704 4705 if self.userInfoFile: 4706 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4707 fH.write(infoText) 4708 4709 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4710 4711 if self.useHTMLReports: 4712 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4713 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4714 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4715 4716 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4717 4718 return view 4719 4720 4721class Args: 4722 """ 4723 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4724 """ 4725 def __init__(self, **kwargs): 4726 self.__dict__.update(kwargs) 4727 4728 def __getattr__(self, item): 4729 return None 4730 4731 4732def ParseArgs(): 4733 """This function get and parse command line keys.""" 4734 parser = ArgumentParser() # command-line string parser 4735 4736 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4737 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4738 4739 # --- options: 4740 4741 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4742 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4743 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4744 4745 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4746 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4747 4748 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4749 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4750 4751 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4752 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4753 4754 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4755 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4756 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4757 4758 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4759 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4760 4761 # --- commands: 4762 4763 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4764 4765 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4766 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4767 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4768 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4769 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4770 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4771 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4772 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4773 4774 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4775 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4776 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4777 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4778 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4779 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4780 4781 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4782 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4783 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4784 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4785 4786 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4787 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4788 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4789 4790 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4791 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4792 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4793 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4794 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4795 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4796 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4797 4798 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4799 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4800 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4801 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4802 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4803 4804 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4805 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4806 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4807 4808 cmdArgs = parser.parse_args() 4809 return cmdArgs 4810 4811 4812def Main(**kwargs): 4813 """ 4814 Main function for work with TKSBrokerAPI in the console. 4815 4816 See examples: 4817 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4818 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4819 """ 4820 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4821 4822 if args.debug_level: 4823 uLogger.level = 10 # always debug level by default 4824 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4825 4826 exitCode = 0 4827 start = datetime.now(tzutc()) 4828 uLogger.debug("=-" * 50) 4829 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4830 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4831 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4832 )) 4833 4834 # trying to calculate full current version: 4835 buildVersion = __version__ 4836 try: 4837 v = version("tksbrokerapi") 4838 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4839 4840 except Exception: 4841 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4842 4843 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4844 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4845 4846 try: 4847 if args.version: 4848 print("TKSBrokerAPI {}".format(buildVersion)) 4849 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4850 4851 else: 4852 # Init class for trading with Tinkoff Broker: 4853 trader = TinkoffBrokerServer( 4854 token=args.token, 4855 accountId=args.account_id, 4856 useCache=not args.no_cache, 4857 ) 4858 4859 # --- set some options: 4860 4861 if args.more: 4862 trader.moreDebug = True 4863 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4864 4865 if args.html: 4866 trader.useHTMLReports = True 4867 4868 if args.ticker: 4869 ticker = str(args.ticker).upper() # Tickers may be upper case only 4870 4871 if ticker in trader.aliasesKeys: 4872 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4873 4874 else: 4875 trader.ticker = ticker 4876 4877 if args.figi: 4878 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4879 4880 if args.depth is not None: 4881 trader.depth = args.depth 4882 4883 # --- do one command: 4884 4885 if args.list: 4886 if args.output is not None: 4887 trader.instrumentsFile = args.output 4888 4889 trader.ShowInstrumentsInfo(show=True) 4890 4891 elif args.list_xlsx: 4892 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4893 4894 elif args.bonds_xlsx is not None: 4895 if args.output is not None: 4896 trader.bondsXLSXFile = args.output 4897 4898 if len(args.bonds_xlsx) == 0: 4899 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4900 4901 else: 4902 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4903 4904 elif args.search: 4905 if args.output is not None: 4906 trader.searchResultsFile = args.output 4907 4908 trader.SearchInstruments(pattern=args.search[0], show=True) 4909 4910 elif args.info: 4911 if not (args.ticker or args.figi): 4912 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4913 raise Exception("Ticker or FIGI required") 4914 4915 if args.output is not None: 4916 trader.infoFile = args.output 4917 4918 if args.ticker: 4919 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4920 4921 else: 4922 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4923 4924 elif args.calendar is not None: 4925 if args.output is not None: 4926 trader.calendarFile = args.output 4927 4928 if len(args.calendar) == 0: 4929 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4930 4931 else: 4932 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4933 4934 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4935 4936 elif args.price: 4937 if not (args.ticker or args.figi): 4938 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4939 raise Exception("Ticker or FIGI required") 4940 4941 trader.GetCurrentPrices(show=True) 4942 4943 elif args.prices is not None: 4944 if args.output is not None: 4945 trader.pricesFile = args.output 4946 4947 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4948 4949 elif args.overview: 4950 if args.output is not None: 4951 trader.overviewFile = args.output 4952 4953 trader.Overview(show=True, details="full") 4954 4955 elif args.overview_digest: 4956 if args.output is not None: 4957 trader.overviewDigestFile = args.output 4958 4959 trader.Overview(show=True, details="digest") 4960 4961 elif args.overview_positions: 4962 if args.output is not None: 4963 trader.overviewPositionsFile = args.output 4964 4965 trader.Overview(show=True, details="positions") 4966 4967 elif args.overview_orders: 4968 if args.output is not None: 4969 trader.overviewOrdersFile = args.output 4970 4971 trader.Overview(show=True, details="orders") 4972 4973 elif args.overview_analytics: 4974 if args.output is not None: 4975 trader.overviewAnalyticsFile = args.output 4976 4977 trader.Overview(show=True, details="analytics") 4978 4979 elif args.overview_calendar: 4980 if args.output is not None: 4981 trader.overviewAnalyticsFile = args.output 4982 4983 trader.Overview(show=True, details="calendar") 4984 4985 elif args.deals is not None: 4986 if args.output is not None: 4987 trader.reportFile = args.output 4988 4989 if 0 <= len(args.deals) < 3: 4990 trader.Deals( 4991 start=args.deals[0] if len(args.deals) >= 1 else None, 4992 end=args.deals[1] if len(args.deals) == 2 else None, 4993 show=True, # Always show deals report in console 4994 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4995 ) 4996 4997 else: 4998 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4999 raise Exception("Incorrect value") 5000 5001 elif args.history is not None: 5002 if args.output is not None: 5003 trader.historyFile = args.output 5004 5005 if 0 <= len(args.history) < 3: 5006 dataReceived = trader.History( 5007 start=args.history[0] if len(args.history) >= 1 else None, 5008 end=args.history[1] if len(args.history) == 2 else None, 5009 interval="hour" if args.interval is None or not args.interval else args.interval, 5010 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5011 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5012 show=True, # shows all downloaded candles in console 5013 ) 5014 5015 if args.render_chart is not None and dataReceived is not None: 5016 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5017 5018 trader.ShowHistoryChart( 5019 candles=dataReceived, 5020 interact=iChart, 5021 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5022 ) 5023 5024 else: 5025 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5026 raise Exception("Incorrect value") 5027 5028 elif args.load_history is not None: 5029 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5030 5031 if args.render_chart is not None and histData is not None: 5032 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5033 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5034 5035 trader.ShowHistoryChart( 5036 candles=histData, 5037 interact=iChart, 5038 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5039 ) 5040 5041 elif args.trade is not None: 5042 if 1 <= len(args.trade) <= 5: 5043 trader.Trade( 5044 operation=args.trade[0], 5045 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5046 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5047 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5048 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5049 ) 5050 5051 else: 5052 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5053 5054 elif args.buy is not None: 5055 if 0 <= len(args.buy) <= 4: 5056 trader.Buy( 5057 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5058 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5059 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5060 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5061 ) 5062 5063 else: 5064 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5065 5066 elif args.sell is not None: 5067 if 0 <= len(args.sell) <= 4: 5068 trader.Sell( 5069 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5070 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5071 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5072 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5073 ) 5074 5075 else: 5076 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5077 5078 elif args.order: 5079 if 4 <= len(args.order) <= 7: 5080 trader.Order( 5081 operation=args.order[0], 5082 orderType=args.order[1], 5083 lots=int(args.order[2]), 5084 targetPrice=float(args.order[3]), 5085 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5086 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5087 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5088 ) 5089 5090 else: 5091 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5092 5093 elif args.buy_limit: 5094 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5095 5096 elif args.sell_limit: 5097 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5098 5099 elif args.buy_stop: 5100 if 2 <= len(args.buy_stop) <= 7: 5101 trader.BuyStop( 5102 lots=int(args.buy_stop[0]), 5103 targetPrice=float(args.buy_stop[1]), 5104 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5105 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5106 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5107 ) 5108 5109 else: 5110 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5111 5112 elif args.sell_stop: 5113 if 2 <= len(args.sell_stop) <= 7: 5114 trader.SellStop( 5115 lots=int(args.sell_stop[0]), 5116 targetPrice=float(args.sell_stop[1]), 5117 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5118 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5119 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5120 ) 5121 5122 else: 5123 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5124 5125 # elif args.buy_order_grid is not None: 5126 # # update order grid work with api v2 5127 # if len(args.buy_order_grid) == 2: 5128 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5129 # 5130 # for order in orderParams: 5131 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5132 # 5133 # else: 5134 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5135 # 5136 # elif args.sell_order_grid is not None: 5137 # # update order grid work with api v2 5138 # if len(args.sell_order_grid) >= 2: 5139 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5140 # 5141 # for order in orderParams: 5142 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5143 # 5144 # else: 5145 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5146 5147 elif args.close_order is not None: 5148 trader.CloseOrders(args.close_order) # close only one order 5149 5150 elif args.close_orders is not None: 5151 trader.CloseOrders(args.close_orders) # close list of orders 5152 5153 elif args.close_trade: 5154 if not (args.ticker or args.figi): 5155 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5156 raise Exception("Ticker or FIGI required") 5157 5158 if args.ticker: 5159 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5160 5161 else: 5162 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5163 5164 elif args.close_trades is not None: 5165 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5166 5167 elif args.close_all is not None: 5168 if args.ticker: 5169 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5170 5171 elif args.figi: 5172 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5173 5174 else: 5175 trader.CloseAll(*args.close_all) 5176 5177 elif args.limits: 5178 if args.output is not None: 5179 trader.withdrawalLimitsFile = args.output 5180 5181 trader.OverviewLimits(show=True) 5182 5183 elif args.user_info: 5184 if args.output is not None: 5185 trader.userInfoFile = args.output 5186 5187 trader.OverviewUserInfo(show=True) 5188 5189 elif args.account: 5190 if args.output is not None: 5191 trader.userAccountsFile = args.output 5192 5193 trader.OverviewAccounts(show=True) 5194 5195 else: 5196 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5197 raise Exception("There is no command to execute") 5198 5199 except Exception: 5200 trace = tb.format_exc() 5201 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5202 if e in trace: 5203 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5204 break 5205 5206 uLogger.debug(trace) 5207 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5208 exitCode = 255 # an error occurred, must be open a ticket for this issue 5209 5210 finally: 5211 finish = datetime.now(tzutc()) 5212 5213 if exitCode == 0: 5214 if args.more: 5215 uLogger.debug("All operations were finished success (summary code is 0).") 5216 5217 else: 5218 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5219 os.path.abspath(uLog.defaultLogFile), exitCode, 5220 )) 5221 5222 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5223 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5224 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5225 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5226 )) 5227 uLogger.debug("=-" * 50) 5228 5229 if not kwargs: 5230 sys.exit(exitCode) 5231 5232 else: 5233 return exitCode 5234 5235 5236if __name__ == "__main__": 5237 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """ 360 361 @property 362 def ticker(self) -> str: 363 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 return self._ticker 371 372 @ticker.setter 373 def ticker(self, value): 374 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 375 376 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 377 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 378 379 See also: `SearchByTicker()`, `SearchInstruments()`. 380 """ 381 self._ticker = str(value).upper() # Tickers may be upper case only 382 383 @property 384 def figi(self) -> str: 385 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 386 387 See also: `SearchByFIGI()`, `SearchInstruments()`. 388 """ 389 return self._figi 390 391 @figi.setter 392 def figi(self, value): 393 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 394 395 See also: `SearchByFIGI()`, `SearchInstruments()`. 396 """ 397 self._figi = str(value).upper() # FIGI may be upper case only 398 399 def _ParseJSON(self, rawData="{}") -> dict: 400 """ 401 Parse JSON from response string. 402 403 :param rawData: this is a string with JSON-formatted text. 404 :return: JSON (dictionary), parsed from server response string. 405 """ 406 responseJSON = json.loads(rawData) if rawData else {} 407 408 if self.moreDebug: 409 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 410 411 return responseJSON 412 413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON 510 511 def _IUpdater(self, iType: str) -> tuple: 512 """ 513 Request instrument by type from server. See available API methods for instruments: 514 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 515 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 516 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 517 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 518 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 519 520 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 521 :return: tuple with iType name and list of available instruments of current type for defined user token. 522 """ 523 result = [] 524 525 if iType in TKS_INSTRUMENTS: 526 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 527 528 # all instruments have the same body in API v2 requests: 529 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 530 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 531 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 532 533 return iType, result 534 535 def _IWrapper(self, kwargs): 536 """ 537 Wrapper runs instrument's update method `_IUpdater()`. 538 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 539 """ 540 return self._IUpdater(**kwargs) 541 542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList 579 580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 620 621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump 646 647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :return: multilines text in Markdown format with information about one instrument. 656 """ 657 splitLine = "| | |\n" 658 infoText = "" 659 660 if iJSON is not None and iJSON and isinstance(iJSON, dict): 661 info = [ 662 "# Main information\n\n", 663 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 664 "| Parameters | Values |\n", 665 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 666 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 667 "| Full name: | {:<54} |\n".format(iJSON["name"]), 668 ] 669 670 if "sector" in iJSON.keys() and iJSON["sector"]: 671 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 672 673 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 674 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 675 676 info.extend([ 677 splitLine, 678 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 679 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 680 ]) 681 682 if "isin" in iJSON.keys() and iJSON["isin"]: 683 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 684 685 if "classCode" in iJSON.keys(): 686 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 687 688 info.extend([ 689 splitLine, 690 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 691 splitLine, 692 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 693 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 694 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 695 ]) 696 697 if iJSON["figi"]: 698 self._figi = iJSON["figi"] 699 iJSON = iJSON | self.RequestTradingStatus() 700 701 info.extend([ 702 splitLine, 703 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 704 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 705 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 706 ]) 707 708 info.append(splitLine) 709 710 if "type" in iJSON.keys() and iJSON["type"]: 711 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 712 713 if "shareType" in iJSON.keys() and iJSON["shareType"]: 714 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 715 716 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 717 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 718 719 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 720 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 721 722 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 723 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 724 725 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 726 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 727 728 if "focusType" in iJSON.keys() and iJSON["focusType"]: 729 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 730 731 if "assetType" in iJSON.keys() and iJSON["assetType"]: 732 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 733 734 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 735 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 736 737 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 738 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 739 740 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 741 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 742 743 if "currency" in iJSON.keys(): 744 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 745 746 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 747 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 748 749 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 750 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 751 752 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 753 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 754 755 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 756 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 757 758 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 759 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 760 761 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 762 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 763 764 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 765 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 766 767 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 768 info.append("| Perpetual bond: | Yes |\n") 769 770 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 771 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 772 773 iExt = None 774 if iJSON["type"] == "Bonds": 775 info.extend([ 776 splitLine, 777 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 778 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 779 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 780 iJSON["nominal"]["currency"], 781 )), 782 ]) 783 784 if "floatingCouponFlag" in iJSON.keys(): 785 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 786 787 if "amortizationFlag" in iJSON.keys(): 788 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 789 790 info.append(splitLine) 791 792 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 793 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 794 795 if iJSON["figi"]: 796 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 797 798 info.extend([ 799 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 800 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 801 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 802 ]) 803 804 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 805 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 806 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 807 iJSON["aciValue"]["currency"] 808 ))) 809 810 if "currentPrice" in iJSON.keys(): 811 info.append(splitLine) 812 813 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 814 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 815 816 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 817 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 818 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 819 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 820 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 821 822 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 823 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 824 825 info.extend([ 826 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Changes between last deal price and last close | {:<54} |\n".format( 835 "{:.2f}%{}".format( 836 iJSON["currentPrice"]["changes"], 837 " ({}{:.2f} {})".format( 838 "+" if bondChangesDelta > 0 else "", 839 bondChangesDelta, 840 aciCurrency 841 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 842 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 843 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 844 currency 845 ), 846 ) 847 ), 848 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 849 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 850 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 851 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 854 )), 855 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 859 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 ]) 863 864 if "lot" in iJSON.keys(): 865 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 866 867 if "step" in iJSON.keys() and iJSON["step"] != 0: 868 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 869 870 # Add bond payment calendar: 871 if iJSON["type"] == "Bonds": 872 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 873 info.extend(["\n#", strCalendar]) 874 875 infoText += "".join(info) 876 877 if show: 878 uLogger.info("{}".format(infoText)) 879 880 else: 881 uLogger.debug("{}".format(infoText)) 882 883 if self.infoFile is not None: 884 with open(self.infoFile, "w", encoding="UTF-8") as fH: 885 fH.write(infoText) 886 887 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 888 889 if self.useHTMLReports: 890 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 891 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 892 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 893 894 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 895 896 return infoText 897 898 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 tickerJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 909 910 if not self._ticker: 911 uLogger.warning("self._ticker variable is not be empty!") 912 913 else: 914 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 if self._ticker in self.iList["Shares"].keys(): 922 tickerJSON = self.iList["Shares"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 925 926 elif self._ticker in self.iList["Currencies"].keys(): 927 tickerJSON = self.iList["Currencies"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Bonds"].keys(): 932 tickerJSON = self.iList["Bonds"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Etfs"].keys(): 937 tickerJSON = self.iList["Etfs"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Futures"].keys(): 942 tickerJSON = self.iList["Futures"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 945 946 if tickerJSON: 947 self._figi = tickerJSON["figi"] 948 949 if requestPrice: 950 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 951 952 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 953 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 954 955 else: 956 tickerJSON["currentPrice"]["changes"] = 0 957 958 if show: 959 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 960 961 else: 962 if show: 963 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 964 965 return tickerJSON 966 967 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 968 """ 969 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 970 971 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 972 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 973 :return: JSON formatted data with information about instrument. 974 """ 975 figiJSON = {} 976 if self.moreDebug: 977 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 978 979 if not self._figi: 980 uLogger.warning("self._figi variable is not be empty!") 981 982 else: 983 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 984 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 985 raise Exception("Instrument not allowed") 986 987 if not self.iList: 988 self.iList = self.Listing() 989 990 for item in self.iList["Shares"].keys(): 991 if self._figi == self.iList["Shares"][item]["figi"]: 992 figiJSON = self.iList["Shares"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Currencies"].keys(): 1001 if self._figi == self.iList["Currencies"][item]["figi"]: 1002 figiJSON = self.iList["Currencies"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Bonds"].keys(): 1011 if self._figi == self.iList["Bonds"][item]["figi"]: 1012 figiJSON = self.iList["Bonds"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1016 1017 break 1018 1019 if not figiJSON: 1020 for item in self.iList["Etfs"].keys(): 1021 if self._figi == self.iList["Etfs"][item]["figi"]: 1022 figiJSON = self.iList["Etfs"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Futures"].keys(): 1031 if self._figi == self.iList["Futures"][item]["figi"]: 1032 figiJSON = self.iList["Futures"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1036 1037 break 1038 1039 if figiJSON: 1040 self._figi = figiJSON["figi"] 1041 self._ticker = figiJSON["ticker"] 1042 1043 if requestPrice: 1044 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1045 1046 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1047 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1048 1049 else: 1050 figiJSON["currentPrice"]["changes"] = 0 1051 1052 if show: 1053 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1054 1055 else: 1056 if show: 1057 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1058 1059 return figiJSON 1060 1061 def GetCurrentPrices(self, show: bool = True) -> dict: 1062 """ 1063 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1064 `{"buy": [{"price": 1243.8, "quantity": 193}, 1065 {"price": 1244.0, "quantity": 168}, 1066 {"price": 1244.8, "quantity": 5}, 1067 {"price": 1245.0, "quantity": 61}, 1068 {"price": 1245.4, "quantity": 60}], 1069 "sell": [{"price": 1243.6, "quantity": 8}, 1070 {"price": 1242.6, "quantity": 10}, 1071 {"price": 1242.4, "quantity": 18}, 1072 {"price": 1242.2, "quantity": 50}, 1073 {"price": 1242.0, "quantity": 113}], 1074 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1075 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1076 - sell: list of dicts with Buyers prices, 1077 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1078 - quantity: volume value by current price in lots, 1079 - limitUp: current trade session limit price, maximum, 1080 - limitDown: current trade session limit price, minimum, 1081 - lastPrice: last deal price of the instrument, 1082 - closePrice: previous trade session close price of the instrument. 1083 1084 See also: `SearchByTicker()` and `SearchByFIGI()`. 1085 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1086 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1087 1088 :param show: if `True` then print DOM to log and console. 1089 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1090 If an error occurred then returns an empty record: 1091 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1092 """ 1093 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1094 1095 if self.depth < 1: 1096 uLogger.error("Depth of Market (DOM) must be >=1!") 1097 raise Exception("Incorrect value") 1098 1099 if not (self._ticker or self._figi): 1100 uLogger.error("self._ticker or self._figi variables must be defined!") 1101 raise Exception("Ticker or FIGI required") 1102 1103 if self._ticker and not self._figi: 1104 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1106 1107 if not self._ticker and self._figi: 1108 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1110 1111 if not self._figi: 1112 uLogger.error("FIGI is not defined!") 1113 raise Exception("Ticker or FIGI required") 1114 1115 else: 1116 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1117 1118 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1120 self.body = str({"figi": self._figi, "depth": self.depth}) 1121 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1124 # list of dicts with sellers orders: 1125 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1126 1127 # list of dicts with buyers orders: 1128 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1129 1130 # max price of instrument at this time: 1131 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1132 1133 # min price of instrument at this time: 1134 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1135 1136 # last price of deal with instrument: 1137 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1138 1139 # last close price of instrument: 1140 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1141 1142 else: 1143 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1144 uLogger.debug("Server response: {}".format(pricesResponse)) 1145 1146 if show: 1147 if prices["buy"] or prices["sell"]: 1148 info = [ 1149 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1150 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1151 self._ticker, 1152 self._figi, 1153 self.depth, 1154 ), 1155 "-" * 60, "\n", 1156 " Orders of Buyers | Orders of Sellers\n", 1157 "-" * 60, "\n", 1158 " Sell prices (volumes) | Buy prices (volumes)\n", 1159 "-" * 60, "\n", 1160 ] 1161 1162 if not prices["buy"]: 1163 info.append(" | No orders!\n") 1164 sumBuy = 0 1165 1166 else: 1167 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1168 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1169 for item in maxMinSorted: 1170 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1171 1172 if not prices["sell"]: 1173 info.append("No orders! |\n") 1174 sumSell = 0 1175 1176 else: 1177 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1178 for item in prices["sell"]: 1179 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1180 1181 info.extend([ 1182 "-" * 60, "\n", 1183 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1184 "-" * 60, "\n", 1185 ]) 1186 1187 infoText = "".join(info) 1188 1189 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1190 1191 else: 1192 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1193 1194 return prices 1195 1196 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1197 """ 1198 This method get and show information about all available broker instruments for current user account. 1199 If `instrumentsFile` string is not empty then also save information to this file. 1200 1201 :param show: if `True` then print results to console, if `False` — print only to file. 1202 :return: multi-lines string with all available broker instruments 1203 """ 1204 if not self.iList: 1205 self.iList = self.Listing() 1206 1207 info = [ 1208 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1209 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1210 ] 1211 1212 # add instruments count by type: 1213 for iType in self.iList.keys(): 1214 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1215 1216 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1217 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1218 1219 # generating info tables with all instruments by type: 1220 for iType in self.iList.keys(): 1221 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1222 1223 for instrument in self.iList[iType].keys(): 1224 iName = self.iList[iType][instrument]["name"] # instrument's name 1225 if len(iName) > 57: 1226 iName = "{}...".format(iName[:54]) # right trim for a long string 1227 1228 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1229 self.iList[iType][instrument]["ticker"], 1230 iName, 1231 self.iList[iType][instrument]["figi"], 1232 self.iList[iType][instrument]["currency"], 1233 self.iList[iType][instrument]["lot"], 1234 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1235 )) 1236 1237 infoText = "".join(info) 1238 1239 if show: 1240 uLogger.info(infoText) 1241 1242 if self.instrumentsFile: 1243 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1244 fH.write(infoText) 1245 1246 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1247 1248 if self.useHTMLReports: 1249 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1250 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1251 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1252 1253 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1254 1255 return infoText 1256 1257 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1258 """ 1259 This method search and show information about instruments by part of its ticker, FIGI or name. 1260 If `searchResultsFile` string is not empty then also save information to this file. 1261 1262 :param pattern: string with part of ticker, FIGI or instrument's name. 1263 :param show: if `True` then print results to console, if `False` — return list of result only. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile: 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults 1343 1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs 1388 1389 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1400 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1401 """ 1402 if instruments is None or not instruments: 1403 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1404 raise Exception("Ticker or FIGI required") 1405 1406 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1407 1408 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1409 1410 iList = [] # trying to get info and current prices about all unique instruments: 1411 for self._figi in onlyUniqueFIGIs: 1412 iData = self.SearchByFIGI(requestPrice=True) 1413 iList.append(iData) 1414 1415 self.ShowListOfPrices(iList, show) 1416 1417 return iList 1418 1419 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1420 """ 1421 Show table contains current prices of given instruments. 1422 1423 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1424 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: multilines text in Markdown format as a table contains current prices. 1427 """ 1428 infoText = "" 1429 1430 if show or self.pricesFile: 1431 info = [ 1432 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1433 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1434 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1435 ] 1436 1437 for item in iList: 1438 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1439 item["ticker"], 1440 item["figi"], 1441 item["type"], 1442 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1443 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1444 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1445 "{} / {}".format( 1446 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1447 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1448 ), 1449 "{} / {}".format( 1450 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1451 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1452 ), 1453 item["currency"], 1454 )) 1455 1456 infoText = "".join(info) 1457 1458 if show: 1459 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1460 1461 if self.pricesFile: 1462 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1463 fH.write(infoText) 1464 1465 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1466 1467 if self.useHTMLReports: 1468 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1469 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1470 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1471 1472 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1473 1474 return infoText 1475 1476 def RequestTradingStatus(self) -> dict: 1477 """ 1478 Requesting trading status for the instrument defined by `figi` variable. 1479 1480 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1481 1482 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1483 1484 :return: dictionary with trading status attributes. Response example: 1485 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1486 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1487 """ 1488 if self._figi is None or not self._figi: 1489 uLogger.error("Variable `figi` must be defined for using this method!") 1490 raise Exception("FIGI required") 1491 1492 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1493 1494 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1495 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1496 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1497 1498 if self.moreDebug: 1499 uLogger.debug("Records about current trading status successfully received") 1500 1501 return tradingStatus 1502 1503 def RequestPortfolio(self) -> dict: 1504 """ 1505 Requesting actual user's portfolio for current `accountId`. 1506 1507 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1508 1509 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1510 1511 :return: dictionary with user's portfolio. 1512 """ 1513 if self.accountId is None or not self.accountId: 1514 uLogger.error("Variable `accountId` must be defined for using this method!") 1515 raise Exception("Account ID required") 1516 1517 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1518 1519 self.body = str({"accountId": self.accountId}) 1520 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1521 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1522 1523 if self.moreDebug: 1524 uLogger.debug("Records about user's portfolio successfully received") 1525 1526 return rawPortfolio 1527 1528 def RequestPositions(self) -> dict: 1529 """ 1530 Requesting open positions by currencies and instruments for current `accountId`. 1531 1532 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1533 1534 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1535 1536 :return: dictionary with open positions by instruments. 1537 """ 1538 if self.accountId is None or not self.accountId: 1539 uLogger.error("Variable `accountId` must be defined for using this method!") 1540 raise Exception("Account ID required") 1541 1542 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1543 1544 self.body = str({"accountId": self.accountId}) 1545 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1546 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1547 1548 if self.moreDebug: 1549 uLogger.debug("Records about current open positions successfully received") 1550 1551 return rawPositions 1552 1553 def RequestPendingOrders(self) -> list: 1554 """ 1555 Requesting current actual pending limit orders for current `accountId`. 1556 1557 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1558 1559 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1560 1561 :return: list of dictionaries with pending limit orders. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1571 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1572 1573 if "orders" in rawResponse.keys(): 1574 rawOrders = rawResponse["orders"] 1575 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1576 1577 else: 1578 rawOrders = [] 1579 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1580 1581 return rawOrders 1582 1583 def RequestStopOrders(self) -> list: 1584 """ 1585 Requesting current actual stop orders for current `accountId`. 1586 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1602 1603 if "stopOrders" in rawResponse.keys(): 1604 rawStopOrders = rawResponse["stopOrders"] 1605 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1606 1607 else: 1608 rawStopOrders = [] 1609 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1610 1611 return rawStopOrders 1612 1613 def Overview(self, show: bool = False, details: str = "full") -> dict: 1614 """ 1615 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1616 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1617 and `overviewBondsCalendarFile` are defined then also save information to file. 1618 1619 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1620 many requests about the state of the portfolio, and then, based on the received data, a large number 1621 of calculation and statistics are collected. 1622 1623 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1624 :param details: how detailed should the information be? 1625 - `full` — shows full available information about portfolio status (by default), 1626 - `positions` — shows only open positions, 1627 - `orders` — shows only sections of open limits and stop orders. 1628 - `digest` — show a short digest of the portfolio status, 1629 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1630 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1631 :return: dictionary with client's raw portfolio and some statistics. 1632 """ 1633 if self.accountId is None or not self.accountId: 1634 uLogger.error("Variable `accountId` must be defined for using this method!") 1635 raise Exception("Account ID required") 1636 1637 view = { 1638 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1639 "headers": {}, # list of dictionaries, response headers without "positions" section 1640 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1641 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1642 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1643 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1644 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1645 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1646 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1647 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1648 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1649 }, 1650 "stat": { # --- some statistics calculated using "raw" sections: 1651 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1652 "availableRUB": 0., # available rubles (without other currencies) 1653 "blockedRUB": 0., # blocked sum in Russian Rouble 1654 "totalChangesRUB": 0., # changes for all open trades in RUB 1655 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1656 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1657 "sharesCostRUB": 0., # costs of all shares in RUB 1658 "bondsCostRUB": 0., # costs of all bonds in RUB 1659 "etfsCostRUB": 0., # costs of all etfs in RUB 1660 "futuresCostRUB": 0., # costs of all futures in RUB 1661 "Currencies": [], # list of dictionaries of all currencies statistics 1662 "Shares": [], # list of dictionaries of all shares statistics 1663 "Bonds": [], # list of dictionaries of all bonds statistics 1664 "Etfs": [], # list of dictionaries of all etfs statistics 1665 "Futures": [], # list of dictionaries of all futures statistics 1666 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1667 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1668 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1669 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1670 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1671 }, 1672 "analytics": { # --- some analytics of portfolio: 1673 "distrByAssets": {}, # portfolio distribution by assets 1674 "distrByCompanies": {}, # portfolio distribution by companies 1675 "distrBySectors": {}, # portfolio distribution by sectors 1676 "distrByCurrencies": {}, # portfolio distribution by currencies 1677 "distrByCountries": {}, # portfolio distribution by countries 1678 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1679 } 1680 } 1681 1682 details = details.lower() 1683 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1684 if details not in availableDetails: 1685 details = "full" 1686 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1687 1688 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1689 1690 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1691 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1692 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1693 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1694 1695 # save response headers without "positions" section: 1696 for key in portfolioResponse.keys(): 1697 if key != "positions": 1698 view["raw"]["headers"][key] = portfolioResponse[key] 1699 1700 else: 1701 continue 1702 1703 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1704 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1705 for item in portfolioResponse["positions"]: 1706 if item["instrumentType"] == "currency": 1707 self._figi = item["figi"] 1708 curr = self.SearchByFIGI(requestPrice=False) 1709 1710 # current price of currency in RUB: 1711 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1712 "name": curr["name"], 1713 "currentPrice": NanoToFloat( 1714 item["currentPrice"]["units"], 1715 item["currentPrice"]["nano"] 1716 ), 1717 } 1718 1719 view["raw"]["Currencies"].append(item) 1720 1721 elif item["instrumentType"] == "share": 1722 view["raw"]["Shares"].append(item) 1723 1724 elif item["instrumentType"] == "bond": 1725 view["raw"]["Bonds"].append(item) 1726 1727 elif item["instrumentType"] == "etf": 1728 view["raw"]["Etfs"].append(item) 1729 1730 elif item["instrumentType"] == "futures": 1731 view["raw"]["Futures"].append(item) 1732 1733 else: 1734 continue 1735 1736 # how many volume of currencies (by ISO currency name) are blocked: 1737 for item in view["raw"]["positions"]["blocked"]: 1738 blocked = NanoToFloat(item["units"], item["nano"]) 1739 if blocked > 0: 1740 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1741 1742 # how many volume of instruments (by FIGI) are blocked: 1743 for item in view["raw"]["positions"]["securities"]: 1744 blocked = int(item["blocked"]) 1745 if blocked > 0: 1746 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1747 1748 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1749 1750 if "rub" in allBlocked.keys(): 1751 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1752 1753 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1754 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1755 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1756 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1757 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1758 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1759 view["stat"]["portfolioCostRUB"] = sum([ 1760 view["stat"]["allCurrenciesCostRUB"], 1761 view["stat"]["sharesCostRUB"], 1762 view["stat"]["bondsCostRUB"], 1763 view["stat"]["etfsCostRUB"], 1764 view["stat"]["futuresCostRUB"], 1765 ]) 1766 1767 # --- calculating some portfolio statistics: 1768 byComp = {} # distribution by companies 1769 bySect = {} # distribution by sectors 1770 byCurr = {} # distribution by currencies (include RUB) 1771 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1772 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1773 1774 for item in portfolioResponse["positions"]: 1775 self._figi = item["figi"] 1776 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1777 1778 if instrument: 1779 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1780 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1781 1782 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1783 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1784 1785 else: 1786 blocked = 0 1787 1788 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1789 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1790 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1791 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1792 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1793 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1794 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1795 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1796 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1797 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1798 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1799 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1800 1801 statData = { 1802 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1803 "ticker": instrument["ticker"], # ticker by FIGI 1804 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1805 "volume": volume, # available volume of instrument 1806 "lots": lots, # volume in lots of instrument 1807 "direction": direction, # direction of an instrument's position: short or long 1808 "blocked": blocked, # blocked volume of currency or instrument 1809 "currentPrice": curPrice, # current instrument's price in basic asset 1810 "average": average, # current average position price 1811 "cost": cost, # current cost of all volume of instrument in basic asset 1812 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1813 "costRUB": costRUB, # cost of instrument in ruble 1814 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1815 "profit": profit, # expected profit at current moment 1816 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1817 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1818 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1819 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1820 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1821 "step": instrument["step"], # minimum price increment 1822 } 1823 1824 # adding distribution by unique countries: 1825 if statData["country"] not in byCountry.keys(): 1826 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1827 1828 else: 1829 byCountry[statData["country"]]["cost"] += costRUB 1830 byCountry[statData["country"]]["percent"] += percentCostRUB 1831 1832 if item["instrumentType"] != "currency": 1833 # adding distribution by unique companies: 1834 if statData["name"]: 1835 if statData["name"] not in byComp.keys(): 1836 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 byComp[statData["name"]]["cost"] += costRUB 1840 byComp[statData["name"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique sectors: 1843 if statData["sector"] not in bySect.keys(): 1844 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 bySect[statData["sector"]]["cost"] += costRUB 1848 bySect[statData["sector"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique currencies: 1851 if currency not in byCurr.keys(): 1852 byCurr[currency] = { 1853 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1854 "cost": costRUB, 1855 "percent": percentCostRUB 1856 } 1857 1858 else: 1859 byCurr[currency]["cost"] += costRUB 1860 byCurr[currency]["percent"] += percentCostRUB 1861 1862 # saving statistics for every instrument: 1863 if item["instrumentType"] == "currency": 1864 view["stat"]["Currencies"].append(statData) 1865 1866 # update dict with free funds for trading (total - blocked) by currencies 1867 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1868 view["stat"]["funds"][currency] = { 1869 "total": volume, 1870 "totalCostRUB": costRUB, # total volume cost in rubles 1871 "free": volume - blocked, 1872 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1873 } 1874 1875 elif item["instrumentType"] == "share": 1876 view["stat"]["Shares"].append(statData) 1877 1878 elif item["instrumentType"] == "bond": 1879 view["stat"]["Bonds"].append(statData) 1880 1881 elif item["instrumentType"] == "etf": 1882 view["stat"]["Etfs"].append(statData) 1883 1884 elif item["instrumentType"] == "Futures": 1885 view["stat"]["Futures"].append(statData) 1886 1887 else: 1888 continue 1889 1890 # total changes in Russian Ruble: 1891 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1892 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1893 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1894 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1895 view["stat"]["funds"]["rub"] = { 1896 "total": view["stat"]["availableRUB"], 1897 "totalCostRUB": view["stat"]["availableRUB"], 1898 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1899 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1900 } 1901 1902 # --- pending limit orders sector data: 1903 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1904 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1905 1906 for item in view["raw"]["orders"]: 1907 self._figi = item["figi"] 1908 1909 if item["figi"] not in uniquePendingOrdersFIGIs: 1910 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1911 1912 uniquePendingOrdersFIGIs.append(item["figi"]) 1913 uniquePendingOrders[item["figi"]] = instrument 1914 1915 else: 1916 instrument = uniquePendingOrders[item["figi"]] 1917 1918 if instrument: 1919 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1920 orderType = TKS_ORDER_TYPES[item["orderType"]] 1921 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1922 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1923 1924 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1925 if item["direction"] == "ORDER_DIRECTION_BUY": 1926 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1927 1928 else: 1929 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1930 1931 # requested price for order execution: 1932 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1933 1934 # necessary changes in percent to reach target from current price: 1935 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1936 1937 view["stat"]["orders"].append({ 1938 "orderID": item["orderId"], # orderId number parameter of current order 1939 "figi": item["figi"], # FIGI identification 1940 "ticker": instrument["ticker"], # ticker name by FIGI 1941 "lotsRequested": item["lotsRequested"], # requested lots value 1942 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1943 "currentPrice": lastPrice, # current instrument's price for defined action 1944 "targetPrice": target, # requested price for order execution in base currency 1945 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1946 "percentChanges": changes, # changes in percent to target from current price 1947 "currency": item["currency"], # instrument's currency name 1948 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1949 "type": orderType, # type of order from TKS_ORDER_TYPES 1950 "status": orderState, # order status from TKS_ORDER_STATES 1951 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1952 }) 1953 1954 # --- stop orders sector data: 1955 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1956 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1957 1958 for item in view["raw"]["stopOrders"]: 1959 self._figi = item["figi"] 1960 1961 if item["figi"] not in uniqueStopOrdersFIGIs: 1962 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1963 1964 uniqueStopOrdersFIGIs.append(item["figi"]) 1965 uniqueStopOrders[item["figi"]] = instrument 1966 1967 else: 1968 instrument = uniqueStopOrders[item["figi"]] 1969 1970 if instrument: 1971 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1972 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1973 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1974 1975 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1976 if "expirationTime" in item.keys(): 1977 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1978 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1979 1980 else: 1981 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1982 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1983 1984 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1985 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1986 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1987 1988 else: 1989 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1990 1991 # requested price when stop-order executed: 1992 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1993 1994 # price for limit-order, set up when stop-order executed: 1995 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1996 1997 # necessary changes in percent to reach target from current price: 1998 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1999 2000 view["stat"]["stopOrders"].append({ 2001 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2002 "figi": item["figi"], # FIGI identification 2003 "ticker": instrument["ticker"], # ticker name by FIGI 2004 "lotsRequested": item["lotsRequested"], # requested lots value 2005 "currentPrice": lastPrice, # current instrument's price for defined action 2006 "targetPrice": target, # requested price for stop-order execution in base currency 2007 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2008 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2009 "percentChanges": changes, # changes in percent to target from current price 2010 "currency": item["currency"], # instrument's currency name 2011 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2012 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2013 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2014 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2015 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2016 }) 2017 2018 # --- calculating data for analytics section: 2019 # portfolio distribution by assets: 2020 view["analytics"]["distrByAssets"] = { 2021 "Ruble": { 2022 "uniques": 1, 2023 "cost": view["stat"]["availableRUB"], 2024 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 }, 2026 "Currencies": { 2027 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2028 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2029 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2030 }, 2031 "Shares": { 2032 "uniques": len(view["stat"]["Shares"]), 2033 "cost": view["stat"]["sharesCostRUB"], 2034 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2035 }, 2036 "Bonds": { 2037 "uniques": len(view["stat"]["Bonds"]), 2038 "cost": view["stat"]["bondsCostRUB"], 2039 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2040 }, 2041 "Etfs": { 2042 "uniques": len(view["stat"]["Etfs"]), 2043 "cost": view["stat"]["etfsCostRUB"], 2044 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2045 }, 2046 "Futures": { 2047 "uniques": len(view["stat"]["Futures"]), 2048 "cost": view["stat"]["futuresCostRUB"], 2049 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2050 }, 2051 } 2052 2053 # portfolio distribution by companies: 2054 view["analytics"]["distrByCompanies"]["All money cash"] = { 2055 "ticker": "", 2056 "cost": view["stat"]["allCurrenciesCostRUB"], 2057 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 } 2059 view["analytics"]["distrByCompanies"].update(byComp) 2060 2061 # portfolio distribution by sectors: 2062 view["analytics"]["distrBySectors"]["All money cash"] = { 2063 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2064 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2065 } 2066 view["analytics"]["distrBySectors"].update(bySect) 2067 2068 # portfolio distribution by currencies: 2069 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2070 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2071 2072 if self.moreDebug: 2073 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2074 2075 view["analytics"]["distrByCurrencies"].update(byCurr) 2076 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2077 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2078 2079 # portfolio distribution by countries: 2080 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2081 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCountries"].update(byCountry) 2087 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # --- Prepare text statistics overview in human-readable: 2091 if show: 2092 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2093 2094 # Whatever the value `details`, header not changes: 2095 info = [ 2096 "# Client's portfolio\n\n", 2097 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2098 "* **Account ID:** [{}]\n".format(self.accountId), 2099 ] 2100 2101 if details in ["full", "positions", "digest"]: 2102 info.extend([ 2103 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2104 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2105 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2106 view["stat"]["totalChangesRUB"], 2107 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2108 view["stat"]["totalChangesPercentRUB"], 2109 ), 2110 ]) 2111 2112 if details in ["full", "positions"]: 2113 info.extend([ 2114 "## Open positions\n\n", 2115 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2116 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2117 "| **Ruble:** | {:>31} | | | | | |\n".format( 2118 "{:.2f} ({:.2f}) rub".format( 2119 view["stat"]["availableRUB"], 2120 view["stat"]["blockedRUB"], 2121 ) 2122 ) 2123 ]) 2124 2125 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2126 return [ 2127 "| | | | | | | |\n", 2128 "| {:<27} | | | | | {:>19} | |\n".format( 2129 noTradeStr if noTradeStr else typeStr, 2130 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2131 ), 2132 ] 2133 2134 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2135 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2136 "{} [{}]".format(data["ticker"], data["figi"]), 2137 "{:.2f} ({:.2f}) {}".format( 2138 data["volume"], 2139 data["blocked"], 2140 data["currency"], 2141 ) if isCurr else "{:.0f} ({:.0f})".format( 2142 data["volume"], 2143 data["blocked"], 2144 ), 2145 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2146 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2147 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2148 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2149 "{}{:.2f} {} ({}{:.2f}%)".format( 2150 "+" if data["profit"] > 0 else "", 2151 data["profit"], data["baseCurrencyName"], 2152 "+" if data["percentProfit"] > 0 else "", 2153 data["percentProfit"], 2154 ), 2155 ) 2156 2157 # --- Show currencies section: 2158 if view["stat"]["Currencies"]: 2159 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2160 for item in view["stat"]["Currencies"]: 2161 info.append(_InfoStr(item, isCurr=True)) 2162 2163 else: 2164 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2165 2166 # --- Show shares section: 2167 if view["stat"]["Shares"]: 2168 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2169 2170 for item in view["stat"]["Shares"]: 2171 info.append(_InfoStr(item)) 2172 2173 else: 2174 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2175 2176 # --- Show bonds section: 2177 if view["stat"]["Bonds"]: 2178 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2179 2180 for item in view["stat"]["Bonds"]: 2181 info.append(_InfoStr(item)) 2182 2183 else: 2184 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2185 2186 # --- Show etfs section: 2187 if view["stat"]["Etfs"]: 2188 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2189 2190 for item in view["stat"]["Etfs"]: 2191 info.append(_InfoStr(item)) 2192 2193 else: 2194 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2195 2196 # --- Show futures section: 2197 if view["stat"]["Futures"]: 2198 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2199 2200 for item in view["stat"]["Futures"]: 2201 info.append(_InfoStr(item)) 2202 2203 else: 2204 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2205 2206 if details in ["full", "orders"]: 2207 # --- Show pending limit orders section: 2208 if view["stat"]["orders"]: 2209 info.extend([ 2210 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2211 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2212 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2213 ]) 2214 2215 for item in view["stat"]["orders"]: 2216 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2217 "{} [{}]".format(item["ticker"], item["figi"]), 2218 item["orderID"], 2219 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2220 "{} {} ({}{:.2f}%)".format( 2221 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2222 item["baseCurrencyName"], 2223 "+" if item["percentChanges"] > 0 else "", 2224 float(item["percentChanges"]), 2225 ), 2226 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2227 item["action"], 2228 item["type"], 2229 item["date"], 2230 )) 2231 2232 else: 2233 info.append("\n## Total pending limit-orders: [0]\n") 2234 2235 # --- Show stop orders section: 2236 if view["stat"]["stopOrders"]: 2237 info.extend([ 2238 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2239 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2240 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2241 ]) 2242 2243 for item in view["stat"]["stopOrders"]: 2244 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2245 "{} [{}]".format(item["ticker"], item["figi"]), 2246 item["orderID"], 2247 item["lotsRequested"], 2248 "{} {} ({}{:.2f}%)".format( 2249 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2250 item["baseCurrencyName"], 2251 "+" if item["percentChanges"] > 0 else "", 2252 float(item["percentChanges"]), 2253 ), 2254 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2255 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2256 item["action"], 2257 item["type"], 2258 item["expType"], 2259 item["createDate"], 2260 item["expDate"], 2261 )) 2262 2263 else: 2264 info.append("\n## Total stop-orders: [0]\n") 2265 2266 if details in ["full", "analytics"]: 2267 # -- Show analytics section: 2268 if view["stat"]["portfolioCostRUB"] > 0: 2269 info.extend([ 2270 "\n# Analytics\n\n" 2271 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2272 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2273 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2274 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2275 view["stat"]["totalChangesRUB"], 2276 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2277 view["stat"]["totalChangesPercentRUB"], 2278 ), 2279 "\n## Portfolio distribution by assets\n" 2280 "\n| Type | Uniques | Percent | Current cost |\n", 2281 "|------------------------------------|---------|---------|--------------------|\n", 2282 ]) 2283 2284 for key in view["analytics"]["distrByAssets"].keys(): 2285 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2286 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2287 key, 2288 view["analytics"]["distrByAssets"][key]["uniques"], 2289 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2290 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2291 )) 2292 2293 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2294 2295 info.extend([ 2296 "\n## Portfolio distribution by companies\n" 2297 "\n| Company | Percent | Current cost |\n", 2298 aSepLine, 2299 ]) 2300 2301 for company in view["analytics"]["distrByCompanies"].keys(): 2302 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2303 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2304 "{}{}".format( 2305 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2306 company, 2307 ), 2308 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2309 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2310 )) 2311 2312 info.extend([ 2313 "\n## Portfolio distribution by sectors\n" 2314 "\n| Sector | Percent | Current cost |\n", 2315 aSepLine, 2316 ]) 2317 2318 for sector in view["analytics"]["distrBySectors"].keys(): 2319 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2320 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2321 sector, 2322 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2324 )) 2325 2326 info.extend([ 2327 "\n## Portfolio distribution by currencies\n" 2328 "\n| Instruments currencies | Percent | Current cost |\n", 2329 aSepLine, 2330 ]) 2331 2332 for curr in view["analytics"]["distrByCurrencies"].keys(): 2333 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2334 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2335 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2336 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2338 )) 2339 2340 info.extend([ 2341 "\n## Portfolio distribution by countries\n" 2342 "\n| Assets by country | Percent | Current cost |\n", 2343 aSepLine, 2344 ]) 2345 2346 for country in view["analytics"]["distrByCountries"].keys(): 2347 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2348 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2349 country, 2350 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2351 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2352 )) 2353 2354 if details in ["full", "calendar"]: 2355 # -- Show bonds payment calendar section: 2356 if view["stat"]["Bonds"]: 2357 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2358 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2359 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2360 2361 else: 2362 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2363 2364 infoText = "".join(info) 2365 2366 uLogger.info(infoText) 2367 2368 if details == "full" and self.overviewFile: 2369 filename = self.overviewFile 2370 2371 elif details == "digest" and self.overviewDigestFile: 2372 filename = self.overviewDigestFile 2373 2374 elif details == "positions" and self.overviewPositionsFile: 2375 filename = self.overviewPositionsFile 2376 2377 elif details == "orders" and self.overviewOrdersFile: 2378 filename = self.overviewOrdersFile 2379 2380 elif details == "analytics" and self.overviewAnalyticsFile: 2381 filename = self.overviewAnalyticsFile 2382 2383 elif details == "calendar" and self.overviewBondsCalendarFile: 2384 filename = self.overviewBondsCalendarFile 2385 2386 else: 2387 filename = "" 2388 2389 if filename: 2390 with open(filename, "w", encoding="UTF-8") as fH: 2391 fH.write(infoText) 2392 2393 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2394 2395 if self.useHTMLReports: 2396 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2397 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2398 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2399 2400 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2401 2402 return view 2403 2404 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2405 """ 2406 Returns history operations between two given dates for current `accountId`. 2407 If `reportFile` string is not empty then also save human-readable report. 2408 Shows some statistical data of closed positions. 2409 2410 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2411 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2412 :param show: if `True` then also prints all records to the console. 2413 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2414 :return: original list of dictionaries with history of deals records from API ("operations" key): 2415 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2416 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2417 """ 2418 if self.accountId is None or not self.accountId: 2419 uLogger.error("Variable `accountId` must be defined for using this method!") 2420 raise Exception("Account ID required") 2421 2422 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2423 2424 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2425 2426 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2427 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2428 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2429 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2430 customStat = {} # custom statistics in additional to responseJSON 2431 2432 # --- output report in human-readable format: 2433 if show or self.reportFile: 2434 splitLine1 = "| | | | | |\n" # Summary section 2435 splitLine2 = "| | | | | | | | |\n" # Operations section 2436 nextDay = "" 2437 2438 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2439 2440 if len(ops) > 0: 2441 customStat = { 2442 "opsCount": 0, # total operations count 2443 "buyCount": 0, # buy operations 2444 "sellCount": 0, # sell operations 2445 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2446 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2447 "payIn": {"rub": 0.}, # Deposit brokerage account 2448 "payOut": {"rub": 0.}, # Withdrawals 2449 "divs": {"rub": 0.}, # Dividends income 2450 "coupons": {"rub": 0.}, # Coupon's income 2451 "brokerCom": {"rub": 0.}, # Service commissions 2452 "serviceCom": {"rub": 0.}, # Service commissions 2453 "marginCom": {"rub": 0.}, # Margin commissions 2454 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2455 } 2456 2457 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2458 for item in ops: 2459 if item["state"] == "OPERATION_STATE_EXECUTED": 2460 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2461 2462 # count buy operations: 2463 if "_BUY" in item["operationType"]: 2464 customStat["buyCount"] += 1 2465 2466 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2467 customStat["buyTotal"][item["payment"]["currency"]] += payment 2468 2469 else: 2470 customStat["buyTotal"][item["payment"]["currency"]] = payment 2471 2472 # count sell operations: 2473 elif "_SELL" in item["operationType"]: 2474 customStat["sellCount"] += 1 2475 2476 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2477 customStat["sellTotal"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["sellTotal"][item["payment"]["currency"]] = payment 2481 2482 # count incoming operations: 2483 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2484 if item["payment"]["currency"] in customStat["payIn"].keys(): 2485 customStat["payIn"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["payIn"][item["payment"]["currency"]] = payment 2489 2490 # count withdrawals operations: 2491 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2492 if item["payment"]["currency"] in customStat["payOut"].keys(): 2493 customStat["payOut"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["payOut"][item["payment"]["currency"]] = payment 2497 2498 # count dividends income: 2499 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2500 if item["payment"]["currency"] in customStat["divs"].keys(): 2501 customStat["divs"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["divs"][item["payment"]["currency"]] = payment 2505 2506 # count coupon's income: 2507 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2508 if item["payment"]["currency"] in customStat["coupons"].keys(): 2509 customStat["coupons"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["coupons"][item["payment"]["currency"]] = payment 2513 2514 # count broker commissions: 2515 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2516 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2517 customStat["brokerCom"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["brokerCom"][item["payment"]["currency"]] = payment 2521 2522 # count service commissions: 2523 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2524 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2525 customStat["serviceCom"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["serviceCom"][item["payment"]["currency"]] = payment 2529 2530 # count margin commissions: 2531 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2532 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2533 customStat["marginCom"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["marginCom"][item["payment"]["currency"]] = payment 2537 2538 # count withholding taxes: 2539 elif "_TAX" in item["operationType"]: 2540 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2541 customStat["allTaxes"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["allTaxes"][item["payment"]["currency"]] = payment 2545 2546 else: 2547 continue 2548 2549 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2550 2551 # --- view "Actions" lines: 2552 info.extend([ 2553 "| Report sections | | | | |\n", 2554 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2555 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2556 "| | Buy: {:<22} | {:<28} | | |\n".format( 2557 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2558 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2559 ), 2560 "| | Sell: {:<21} | {:<28} | | |\n".format( 2561 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2562 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2563 ), 2564 ]) 2565 2566 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2567 for key in opsKeys: 2568 if key == "rub": 2569 continue 2570 2571 info.extend([ 2572 "| | | {:<28} | | |\n".format( 2573 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2574 ), 2575 "| | | {:<28} | | |\n".format( 2576 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2577 ), 2578 ]) 2579 2580 info.append(splitLine1) 2581 2582 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2583 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2584 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2587 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2588 ) 2589 2590 # --- view "Payments" lines: 2591 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2592 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2593 2594 for key in paymentsKeys: 2595 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2596 2597 info.append(splitLine1) 2598 2599 # --- view "Commissions and taxes" lines: 2600 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2601 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2602 2603 for key in comKeys: 2604 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2605 2606 info.extend([ 2607 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2608 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2609 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2610 ]) 2611 2612 else: 2613 info.append("Broker returned no operations during this period\n") 2614 2615 # --- view "Operations" section: 2616 for item in ops: 2617 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2618 continue 2619 2620 else: 2621 self._figi = item["figi"] if item["figi"] else "" 2622 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2623 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2624 2625 # group of deals during one day: 2626 if nextDay and item["date"].split("T")[0] != nextDay: 2627 info.append(splitLine2) 2628 nextDay = "" 2629 2630 else: 2631 nextDay = item["date"].split("T")[0] # saving current day for splitting 2632 2633 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2634 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2635 self._figi if self._figi else "—", 2636 instrument["ticker"] if instrument else "—", 2637 instrument["type"] if instrument else "—", 2638 item["quantity"] if int(item["quantity"]) > 0 else "—", 2639 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2640 TKS_OPERATION_STATES[item["state"]], 2641 TKS_OPERATION_TYPES[item["operationType"]], 2642 )) 2643 2644 infoText = "".join(info) 2645 2646 if show: 2647 if self.moreDebug: 2648 uLogger.debug("Records about history of a client's operations successfully received") 2649 2650 uLogger.info(infoText) 2651 2652 if self.reportFile: 2653 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2654 fH.write(infoText) 2655 2656 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2657 2658 if self.useHTMLReports: 2659 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2660 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2661 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2662 2663 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2664 2665 return ops, customStat 2666 2667 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2668 """ 2669 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2670 2671 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2672 Warning! Broker server used ISO UTC time by default. 2673 2674 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2675 Also, `historyFile` used to update history with `onlyMissing` parameter. 2676 2677 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2678 2679 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2680 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2681 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2682 `"hour"`, `"day"`. Default: `"hour"`. 2683 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2684 False by default. Warning! History appends only from last candle to current time 2685 with always update last candle! 2686 :param csvSep: separator if csv-file is used, `,` by default. 2687 :param show: if `True` then also prints Pandas DataFrame to the console. 2688 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2689 `["date", "time", "open", "high", "low", "close", "volume"]`. 2690 """ 2691 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2692 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2693 history = None # empty pandas object for history 2694 2695 if interval not in TKS_CANDLE_INTERVALS.keys(): 2696 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2697 raise Exception("Incorrect value") 2698 2699 if not (self._ticker or self._figi): 2700 uLogger.error("Ticker or FIGI must be defined!") 2701 raise Exception("Ticker or FIGI required") 2702 2703 if self._ticker and not self._figi: 2704 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2705 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2706 2707 if self._figi and not self._ticker: 2708 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2709 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2710 2711 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2712 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2713 if interval.lower() != "day": 2714 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2715 2716 delta = dtEnd - dtStart # current UTC time minus last time in file 2717 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2718 2719 # calculate history length in candles: 2720 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2721 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2722 length += 1 # to avoid fraction time 2723 2724 # calculate data blocks count: 2725 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2726 2727 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2728 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2729 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2730 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2731 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2732 2733 tempOld = None # pandas object for old history, if --only-missing key present 2734 lastTime = None # datetime object of last old candle in file 2735 2736 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2737 uLogger.debug("--only-missing key present, add only last missing candles...") 2738 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2739 2740 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2741 2742 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2743 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2744 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2745 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2746 2747 # get last datetime object from last string in file or minus 1 delta if file is empty: 2748 if len(tempOld) > 0: 2749 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2750 2751 else: 2752 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2753 2754 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2755 2756 responseJSONs = [] # raw history blocks of data 2757 2758 blockEnd = dtEnd 2759 for item in range(blocks): 2760 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2761 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2762 2763 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2764 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2765 )) 2766 2767 if blockStart == blockEnd: 2768 uLogger.debug("Skipped this zero-length block...") 2769 2770 else: 2771 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2772 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2773 self.body = str({ 2774 "figi": self._figi, 2775 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2776 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2777 "interval": TKS_CANDLE_INTERVALS[interval][0] 2778 }) 2779 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2780 2781 if "code" in responseJSON.keys(): 2782 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2783 2784 else: 2785 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2786 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2787 2788 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2789 2790 blockEnd = blockStart 2791 2792 printCount = len(responseJSONs) # candles to show in console 2793 if responseJSONs: 2794 tempHistory = pd.DataFrame( 2795 data={ 2796 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2797 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2798 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2799 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2800 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2801 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2802 "volume": [int(item["volume"]) for item in responseJSONs], 2803 }, 2804 index=range(len(responseJSONs)), 2805 columns=["date", "time", "open", "high", "low", "close", "volume"], 2806 ) 2807 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2808 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2809 2810 # append only newest candles to old history if --only-missing key present: 2811 if onlyMissing and tempOld is not None and lastTime is not None: 2812 index = 0 # find start index in tempHistory data: 2813 2814 for i, item in tempHistory.iterrows(): 2815 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2816 2817 if curTime == lastTime: 2818 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2819 index = i 2820 printCount = index + 1 2821 break 2822 2823 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2824 2825 else: 2826 history = tempHistory # if no `--only-missing` key then load full data from server 2827 2828 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2829 2830 if history is not None and not history.empty: 2831 if show: 2832 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2833 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2834 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2835 )) 2836 2837 else: 2838 uLogger.warning("Received an empty candles history!") 2839 2840 if self.historyFile is not None: 2841 if history is not None and not history.empty: 2842 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2843 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2844 2845 else: 2846 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2847 2848 else: 2849 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2850 2851 return history 2852 2853 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2854 """ 2855 Load candles history from csv-file and return Pandas DataFrame object. 2856 2857 See also: `History()` and `ShowHistoryChart()` methods. 2858 2859 :param filePath: path to csv-file to open. 2860 """ 2861 loadedHistory = None # init candles data object 2862 2863 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2864 2865 if os.path.exists(filePath): 2866 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2867 2868 tfStr = self.priceModel.FormattedDelta( 2869 self.priceModel.timeframe, 2870 "{days} days {hours}h {minutes}m {seconds}s", 2871 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2872 self.priceModel.timeframe, 2873 "{hours}h {minutes}m {seconds}s", 2874 ) 2875 2876 if loadedHistory is not None and not loadedHistory.empty: 2877 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2878 len(loadedHistory), 2879 tfStr, 2880 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2881 ) 2882 2883 else: 2884 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2885 2886 else: 2887 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2888 2889 return loadedHistory 2890 2891 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2892 """ 2893 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2894 2895 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2896 Default: `index.html` (both for interact and non-interact candlesticks chart). 2897 2898 See also: `History()` and `LoadHistory()` methods. 2899 2900 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2901 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2902 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2903 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2904 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2905 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2906 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2907 """ 2908 if isinstance(candles, str): 2909 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2910 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2911 2912 elif isinstance(candles, pd.DataFrame): 2913 self.priceModel.prices = candles # set candles chain from variable 2914 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2915 2916 if "datetime" not in candles.columns: 2917 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2918 2919 else: 2920 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2921 raise Exception("Incorrect value") 2922 2923 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2924 2925 if interact: 2926 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2927 2928 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2929 2930 else: 2931 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2932 2933 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2934 2935 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2936 2937 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2938 """ 2939 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2940 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2941 2942 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2943 2944 :param operation: string "Buy" or "Sell". 2945 :param lots: volume, integer count of lots >= 1. 2946 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2947 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2948 :param expDate: string "Undefined" by default or local date in future, 2949 it is a string with format `%Y-%m-%d %H:%M:%S`. 2950 :return: JSON with response from broker server. 2951 """ 2952 if self.accountId is None or not self.accountId: 2953 uLogger.error("Variable `accountId` must be defined for using this method!") 2954 raise Exception("Account ID required") 2955 2956 if operation is None or not operation or operation not in ("Buy", "Sell"): 2957 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2958 raise Exception("Incorrect value") 2959 2960 if lots is None or lots < 1: 2961 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2962 lots = 1 2963 2964 if tp is None or tp < 0: 2965 tp = 0 2966 2967 if sl is None or sl < 0: 2968 sl = 0 2969 2970 if expDate is None or not expDate: 2971 expDate = "Undefined" 2972 2973 if not (self._ticker or self._figi): 2974 uLogger.error("Ticker or FIGI must be defined!") 2975 raise Exception("Ticker or FIGI required") 2976 2977 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2978 self._ticker = instrument["ticker"] 2979 self._figi = instrument["figi"] 2980 2981 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2982 2983 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2984 self.body = str({ 2985 "figi": self._figi, 2986 "quantity": str(lots), 2987 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2988 "accountId": str(self.accountId), 2989 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2990 }) 2991 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2992 2993 if "orderId" in response.keys(): 2994 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2995 operation, response["orderId"], 2996 self._ticker, self._figi, lots, 2997 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2998 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2999 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3000 )) 3001 3002 if tp > 0: 3003 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3004 3005 if sl > 0: 3006 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3007 3008 else: 3009 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3010 3011 return response 3012 3013 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3014 """ 3015 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3016 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3017 3018 See also: `Order()` and `Trade()` docstrings. 3019 3020 :param lots: volume, integer count of lots >= 1. 3021 :param tp: float > 0, take profit price of stop-order. 3022 :param sl: float > 0, stop loss price of stop-order. 3023 :param expDate: it's a local date in future. 3024 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3025 :return: JSON with response from broker server. 3026 """ 3027 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3028 3029 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3030 """ 3031 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3032 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3033 3034 See also: `Order()` and `Trade()` docstrings. 3035 3036 :param lots: volume, integer count of lots >= 1. 3037 :param tp: float > 0, take profit price of stop-order. 3038 :param sl: float > 0, stop loss price of stop-order. 3039 :param expDate: it's a local date in the future. 3040 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3041 :return: JSON with response from broker server. 3042 """ 3043 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3044 3045 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3046 """ 3047 Close position of given instruments. 3048 3049 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3050 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3051 This avoids unnecessary downloading data from the server. 3052 """ 3053 if instruments is None or not instruments: 3054 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3055 raise Exception("Ticker or FIGI required") 3056 3057 if isinstance(instruments, str): 3058 instruments = [instruments] 3059 3060 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3061 if uniqueInstruments: 3062 if portfolio is None or not portfolio: 3063 portfolio = self.Overview(show=False) 3064 3065 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3066 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3067 3068 for self._figi in uniqueInstruments: 3069 if self._figi not in allOpened: 3070 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3071 continue 3072 3073 # search open trade info about instrument by ticker: 3074 instrument = {} 3075 for iType in TKS_INSTRUMENTS: 3076 if instrument: 3077 break 3078 3079 for item in portfolio["stat"][iType]: 3080 if item["figi"] == self._figi: 3081 instrument = item 3082 break 3083 3084 if instrument: 3085 self._ticker = instrument["ticker"] 3086 self._figi = instrument["figi"] 3087 3088 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3089 self._ticker, 3090 self._figi, 3091 int(instrument["volume"]), 3092 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3093 )) 3094 3095 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3096 3097 if tradeLots > 0: 3098 if instrument["blocked"] > 0: 3099 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3100 instrument["blocked"], 3101 self._ticker, 3102 tradeLots, 3103 )) 3104 3105 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3106 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3107 3108 else: 3109 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3110 3111 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3112 """ 3113 Close all positions of given instruments with defined type. 3114 3115 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3116 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3117 This avoids unnecessary downloading data from the server. 3118 """ 3119 if iType not in TKS_INSTRUMENTS: 3120 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3121 3122 else: 3123 if portfolio is None or not portfolio: 3124 portfolio = self.Overview(show=False) 3125 3126 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3127 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3128 3129 if tickers and portfolio: 3130 self.CloseTrades(tickers, portfolio) 3131 3132 else: 3133 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3134 3135 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3136 """ 3137 Universal method to create market or limit orders with all available parameters for current `accountId`. 3138 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3139 3140 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3141 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3142 3143 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3144 then broker immediately open market order as you can do simple --buy or --sell operations! 3145 3146 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3147 When current price will go up or down to target price value then broker opens a limit order. 3148 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3149 3150 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3151 3152 :param operation: string "Buy" or "Sell". 3153 :param orderType: string "Limit" or "Stop". 3154 :param lots: volume, integer count of lots >= 1. 3155 :param targetPrice: target price > 0. This is open trade price for limit order. 3156 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3157 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3158 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3159 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3160 Stop loss order always executed by market price. 3161 :param expDate: string "Undefined" by default or local date in future. 3162 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3163 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3164 A limit order has no expiration date, it lasts until the end of the trading day. 3165 :return: JSON with response from broker server. 3166 """ 3167 if self.accountId is None or not self.accountId: 3168 uLogger.error("Variable `accountId` must be defined for using this method!") 3169 raise Exception("Account ID required") 3170 3171 if operation is None or not operation or operation not in ("Buy", "Sell"): 3172 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3173 raise Exception("Incorrect value") 3174 3175 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3176 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3177 raise Exception("Incorrect value") 3178 3179 if lots is None or lots < 1: 3180 uLogger.error("You must define trade volume > 0: integer count of lots!") 3181 raise Exception("Incorrect value") 3182 3183 if targetPrice is None or targetPrice <= 0: 3184 uLogger.error("Target price for limit-order must be greater than 0!") 3185 raise Exception("Incorrect value") 3186 3187 if limitPrice is None or limitPrice <= 0: 3188 limitPrice = targetPrice 3189 3190 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3191 stopType = "Limit" 3192 3193 if expDate is None or not expDate: 3194 expDate = "Undefined" 3195 3196 if not (self._ticker or self._figi): 3197 uLogger.error("Tocker or FIGI must be defined!") 3198 raise Exception("Ticker or FIGI required") 3199 3200 response = {} 3201 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3202 self._ticker = instrument["ticker"] 3203 self._figi = instrument["figi"] 3204 3205 if orderType == "Limit": 3206 uLogger.debug( 3207 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3208 self._ticker, self._figi, 3209 operation, lots, targetPrice, instrument["currency"], 3210 )) 3211 3212 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3213 self.body = str({ 3214 "figi": self._figi, 3215 "quantity": str(lots), 3216 "price": FloatToNano(targetPrice), 3217 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3218 "accountId": str(self.accountId), 3219 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3220 }) 3221 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3222 3223 if "orderId" in response.keys(): 3224 uLogger.info( 3225 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3226 response["orderId"], self._ticker, self._figi, operation, lots, 3227 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3228 )) 3229 3230 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3231 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3232 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3238 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3239 targetPrice, instrument["currency"], 3240 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3241 )) 3242 3243 else: 3244 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3245 3246 if orderType == "Stop": 3247 uLogger.debug( 3248 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3249 self._ticker, self._figi, 3250 operation, lots, 3251 targetPrice, instrument["currency"], 3252 limitPrice, instrument["currency"], 3253 stopType, expDate, 3254 )) 3255 3256 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3257 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3258 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3259 3260 body = { 3261 "figi": self._figi, 3262 "quantity": str(lots), 3263 "price": FloatToNano(limitPrice), 3264 "stopPrice": FloatToNano(targetPrice), 3265 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3266 "accountId": str(self.accountId), 3267 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3268 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3269 } 3270 3271 if expDateUTC: 3272 body["expireDate"] = expDateUTC 3273 3274 self.body = str(body) 3275 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3276 3277 if "stopOrderId" in response.keys(): 3278 uLogger.info( 3279 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3280 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3281 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3282 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3283 TKS_STOP_ORDER_TYPES[stopOrderType], 3284 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3285 )) 3286 3287 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3288 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3289 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3290 targetPrice, instrument["currency"], 3291 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3292 )) 3293 3294 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3295 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3296 targetPrice, instrument["currency"], 3297 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3298 )) 3299 3300 else: 3301 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3302 3303 return response 3304 3305 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3306 """ 3307 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3308 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3309 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3310 See also: `Order()` docstring. 3311 3312 :param lots: volume, integer count of lots >= 1. 3313 :param targetPrice: target price > 0. This is open trade price for limit order. 3314 :return: JSON with response from broker server. 3315 """ 3316 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3317 3318 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3319 """ 3320 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3321 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3322 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3323 target price value then broker opens a limit order. See also: `Order()` docstring. 3324 3325 :param lots: volume, integer count of lots >= 1. 3326 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3327 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3328 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3329 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3330 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3331 :param expDate: string "Undefined" by default or local date in future. 3332 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3333 This date is converting to UTC format for server. 3334 :return: JSON with response from broker server. 3335 """ 3336 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3337 3338 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3339 """ 3340 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3341 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3342 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3343 See also: `Order()` docstring. 3344 3345 :param lots: volume, integer count of lots >= 1. 3346 :param targetPrice: target price > 0. This is open trade price for limit order. 3347 :return: JSON with response from broker server. 3348 """ 3349 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3350 3351 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3352 """ 3353 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3354 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3355 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3356 target price value then broker opens a limit order. See also: `Order()` docstring. 3357 3358 :param lots: volume, integer count of lots >= 1. 3359 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3360 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3361 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3362 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3363 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3364 :param expDate: string "Undefined" by default or local date in future. 3365 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3366 This date is converting to UTC format for server. 3367 :return: JSON with response from broker server. 3368 """ 3369 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3370 3371 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3372 """ 3373 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3374 3375 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3376 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3377 This avoids unnecessary downloading data from the server. 3378 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3379 """ 3380 if self.accountId is None or not self.accountId: 3381 uLogger.error("Variable `accountId` must be defined for using this method!") 3382 raise Exception("Account ID required") 3383 3384 if orderIDs: 3385 if allOrdersIDs is None: 3386 rawOrders = self.RequestPendingOrders() 3387 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3388 3389 if allStopOrdersIDs is None: 3390 rawStopOrders = self.RequestStopOrders() 3391 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3392 3393 for orderID in orderIDs: 3394 idInPendingOrders = orderID in allOrdersIDs 3395 idInStopOrders = orderID in allStopOrdersIDs 3396 3397 if not (idInPendingOrders or idInStopOrders): 3398 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3399 continue 3400 3401 else: 3402 if idInPendingOrders: 3403 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3404 3405 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3406 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3407 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3408 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3409 3410 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3411 if self.moreDebug: 3412 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3413 3414 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3415 3416 else: 3417 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3418 3419 elif idInStopOrders: 3420 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3421 3422 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3423 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3424 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3425 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3426 3427 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3428 if self.moreDebug: 3429 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3430 3431 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3432 3433 else: 3434 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3435 3436 else: 3437 continue 3438 3439 def CloseAllOrders(self) -> None: 3440 """ 3441 Gets a list of open pending and stop orders and cancel it all. 3442 """ 3443 rawOrders = self.RequestPendingOrders() 3444 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3445 lenOrders = len(allOrdersIDs) 3446 3447 rawStopOrders = self.RequestStopOrders() 3448 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3449 lenSOrders = len(allStopOrdersIDs) 3450 3451 if lenOrders > 0 or lenSOrders > 0: 3452 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3453 3454 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3455 3456 else: 3457 uLogger.info("Orders not found, nothing to cancel.") 3458 3459 def CloseAll(self, *args) -> None: 3460 """ 3461 Close all available (not blocked) opened trades and orders. 3462 3463 Also, you can select one or more keywords case-insensitive: 3464 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3465 3466 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3467 """ 3468 overview = self.Overview(show=False) # get all open trades info 3469 3470 if len(args) == 0: 3471 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3472 self.CloseAllOrders() # close all pending and stop orders 3473 3474 for iType in TKS_INSTRUMENTS: 3475 if iType != "Currencies": 3476 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3477 3478 else: 3479 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3480 lowerArgs = [x.lower() for x in args] 3481 3482 if "orders" in lowerArgs: 3483 self.CloseAllOrders() # close all pending and stop orders 3484 3485 for iType in TKS_INSTRUMENTS: 3486 if iType.lower() in lowerArgs and iType != "Currencies": 3487 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3488 3489 def CloseAllByTicker(self, instrument: str) -> None: 3490 """ 3491 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3492 3493 This method searches opened trade and orders of instrument throw all portfolio and then use 3494 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3495 3496 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3497 3498 :param instrument: string with ticker. 3499 """ 3500 if instrument is None or not instrument: 3501 uLogger.error("Ticker name must be defined for using this method!") 3502 raise Exception("Ticker required") 3503 3504 overview = self.Overview(show=False) # get user portfolio with all open trades info 3505 3506 self._ticker = instrument # try to set instrument as ticker 3507 self._figi = "" 3508 3509 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3510 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3511 3512 if limitAll and self.IsInLimitOrders(portfolio=overview): 3513 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3514 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3515 3516 if stopAll and self.IsInStopOrders(portfolio=overview): 3517 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3518 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3519 3520 if self.IsInPortfolio(portfolio=overview): 3521 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3522 self.CloseTrades(instruments=[instrument], portfolio=overview) 3523 3524 def CloseAllByFIGI(self, instrument: str) -> None: 3525 """ 3526 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3527 3528 This method searches opened trade and orders of instrument throw all portfolio and then use 3529 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3530 3531 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3532 3533 :param instrument: string with FIGI id. 3534 """ 3535 if instrument is None or not instrument: 3536 uLogger.error("FIGI id must be defined for using this method!") 3537 raise Exception("FIGI required") 3538 3539 overview = self.Overview(show=False) # get user portfolio with all open trades info 3540 3541 self._ticker = "" 3542 self._figi = instrument # try to set instrument as FIGI id 3543 3544 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3545 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3546 3547 if limitAll and self.IsInLimitOrders(portfolio=overview): 3548 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3549 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3550 3551 if stopAll and self.IsInStopOrders(portfolio=overview): 3552 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3553 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3554 3555 if self.IsInPortfolio(portfolio=overview): 3556 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3557 self.CloseTrades(instruments=[instrument], portfolio=overview) 3558 3559 @staticmethod 3560 def ParseOrderParameters(operation, **inputParameters): 3561 """ 3562 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3563 3564 :param operation: string "Buy" or "Sell". 3565 :param inputParameters: this is dict of strings that looks like this 3566 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3567 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3568 "prices" key: one or more prices to open limit-orders 3569 Counts of values in lots and prices lists must be equals! 3570 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3571 """ 3572 # TODO: update order grid work with api v2 3573 pass 3574 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3575 # 3576 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3577 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3578 # raise Exception("Incorrect value") 3579 # 3580 # if "l" in inputParameters.keys(): 3581 # inputParameters["lots"] = inputParameters.pop("l") 3582 # 3583 # if "p" in inputParameters.keys(): 3584 # inputParameters["prices"] = inputParameters.pop("p") 3585 # 3586 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3587 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3588 # raise Exception("Incorrect value") 3589 # 3590 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3591 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3592 # 3593 # if len(lots) != len(prices): 3594 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3595 # raise Exception("Incorrect value") 3596 # 3597 # uLogger.debug("Extracted parameters for orders:") 3598 # uLogger.debug("lots = {}".format(lots)) 3599 # uLogger.debug("prices = {}".format(prices)) 3600 # 3601 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3602 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3603 # uLogger.debug("Order parameters: {}".format(result)) 3604 # 3605 # return result 3606 3607 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3608 """ 3609 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3610 3611 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3612 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3613 """ 3614 result = False 3615 msg = "Instrument not defined!" 3616 3617 if portfolio is None or not portfolio: 3618 portfolio = self.Overview(show=False) 3619 3620 if self._ticker: 3621 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3622 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3623 3624 for iType in TKS_INSTRUMENTS: 3625 for instrument in portfolio["stat"][iType]: 3626 if instrument["ticker"] == self._ticker: 3627 result = True 3628 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3629 break 3630 3631 elif self._figi: 3632 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3633 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3634 3635 for iType in TKS_INSTRUMENTS: 3636 for instrument in portfolio["stat"][iType]: 3637 if instrument["figi"] == self._figi: 3638 result = True 3639 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3640 break 3641 3642 else: 3643 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3644 3645 uLogger.debug(msg) 3646 3647 return result 3648 3649 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3650 """ 3651 Returns instrument from the user's portfolio if it presents there. 3652 Instrument must be defined by `ticker` (highly priority) or `figi`. 3653 3654 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3655 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3656 """ 3657 result = None 3658 msg = "Instrument not defined!" 3659 3660 if portfolio is None or not portfolio: 3661 portfolio = self.Overview(show=False) 3662 3663 if self._ticker: 3664 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3665 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3666 3667 for iType in TKS_INSTRUMENTS: 3668 for instrument in portfolio["stat"][iType]: 3669 if instrument["ticker"] == self._ticker: 3670 result = instrument 3671 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3672 break 3673 3674 elif self._figi: 3675 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3676 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3677 3678 for iType in TKS_INSTRUMENTS: 3679 for instrument in portfolio["stat"][iType]: 3680 if instrument["figi"] == self._figi: 3681 result = instrument 3682 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3683 break 3684 3685 else: 3686 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3687 3688 uLogger.debug(msg) 3689 3690 return result 3691 3692 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3693 """ 3694 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3695 3696 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3697 3698 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3699 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3700 """ 3701 result = False 3702 msg = "Instrument not defined!" 3703 3704 if portfolio is None or not portfolio: 3705 portfolio = self.Overview(show=False) 3706 3707 if self._ticker: 3708 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3709 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3710 3711 for instrument in portfolio["stat"]["orders"]: 3712 if instrument["ticker"] == self._ticker: 3713 result = True 3714 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3715 break 3716 3717 elif self._figi: 3718 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3719 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3720 3721 for instrument in portfolio["stat"]["orders"]: 3722 if instrument["figi"] == self._figi: 3723 result = True 3724 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3725 break 3726 3727 else: 3728 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3729 3730 uLogger.debug(msg) 3731 3732 return result 3733 3734 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3735 """ 3736 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3737 Instrument must be defined by `ticker` (highly priority) or `figi`. 3738 3739 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3740 3741 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3742 :return: list with `orderID`s of limit orders. 3743 """ 3744 result = [] 3745 msg = "Instrument not defined!" 3746 3747 if portfolio is None or not portfolio: 3748 portfolio = self.Overview(show=False) 3749 3750 if self._ticker: 3751 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3752 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3753 3754 for instrument in portfolio["stat"]["orders"]: 3755 if instrument["ticker"] == self._ticker: 3756 result.append(instrument["orderID"]) 3757 3758 if result: 3759 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3760 3761 elif self._figi: 3762 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3763 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3764 3765 for instrument in portfolio["stat"]["orders"]: 3766 if instrument["figi"] == self._figi: 3767 result.append(instrument["orderID"]) 3768 3769 if result: 3770 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3771 3772 else: 3773 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3774 3775 uLogger.debug(msg) 3776 3777 return result 3778 3779 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3780 """ 3781 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3782 3783 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3784 3785 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3786 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3787 """ 3788 result = False 3789 msg = "Instrument not defined!" 3790 3791 if portfolio is None or not portfolio: 3792 portfolio = self.Overview(show=False) 3793 3794 if self._ticker: 3795 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3796 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3797 3798 for instrument in portfolio["stat"]["stopOrders"]: 3799 if instrument["ticker"] == self._ticker: 3800 result = True 3801 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3802 break 3803 3804 elif self._figi: 3805 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3806 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3807 3808 for instrument in portfolio["stat"]["stopOrders"]: 3809 if instrument["figi"] == self._figi: 3810 result = True 3811 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3812 break 3813 3814 else: 3815 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3816 3817 uLogger.debug(msg) 3818 3819 return result 3820 3821 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3822 """ 3823 Returns list with all `orderID`s of opened stop orders for the instrument. 3824 Instrument must be defined by `ticker` (highly priority) or `figi`. 3825 3826 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3827 3828 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3829 :return: list with `orderID`s of stop orders. 3830 """ 3831 result = [] 3832 msg = "Instrument not defined!" 3833 3834 if portfolio is None or not portfolio: 3835 portfolio = self.Overview(show=False) 3836 3837 if self._ticker: 3838 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3839 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3840 3841 for instrument in portfolio["stat"]["stopOrders"]: 3842 if instrument["ticker"] == self._ticker: 3843 result.append(instrument["orderID"]) 3844 3845 if result: 3846 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3847 3848 elif self._figi: 3849 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3850 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3851 3852 for instrument in portfolio["stat"]["stopOrders"]: 3853 if instrument["figi"] == self._figi: 3854 result.append(instrument["orderID"]) 3855 3856 if result: 3857 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3858 3859 else: 3860 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3861 3862 uLogger.debug(msg) 3863 3864 return result 3865 3866 def RequestLimits(self) -> dict: 3867 """ 3868 Method for obtaining the available funds for withdrawal for current `accountId`. 3869 3870 See also: 3871 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3872 - `OverviewLimits()` method 3873 3874 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3875 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3876 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3877 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3878 """ 3879 if self.accountId is None or not self.accountId: 3880 uLogger.error("Variable `accountId` must be defined for using this method!") 3881 raise Exception("Account ID required") 3882 3883 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3884 3885 self.body = str({"accountId": self.accountId}) 3886 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3887 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3888 3889 if self.moreDebug: 3890 uLogger.debug("Records about available funds for withdrawal successfully received") 3891 3892 return rawLimits 3893 3894 def OverviewLimits(self, show: bool = False) -> dict: 3895 """ 3896 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3897 3898 See also: `RequestLimits()`. 3899 3900 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3901 :return: dict with raw parsed data from server and some calculated statistics about it. 3902 """ 3903 if self.accountId is None or not self.accountId: 3904 uLogger.error("Variable `accountId` must be defined for using this method!") 3905 raise Exception("Account ID required") 3906 3907 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3908 3909 view = { 3910 "rawLimits": rawLimits, 3911 "limits": { # parsed data for every currency: 3912 "money": { # this is an array of portfolio currency positions 3913 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3914 }, 3915 "blocked": { # this is an array of blocked currency 3916 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3917 }, 3918 "blockedGuarantee": { # this is locked money under collateral for futures 3919 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3920 }, 3921 }, 3922 } 3923 3924 # --- Prepare text table with limits in human-readable format: 3925 if show: 3926 info = [ 3927 "# Withdrawal limits\n\n", 3928 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3929 "* **Account ID:** [{}]\n".format(self.accountId), 3930 ] 3931 3932 if view["limits"]["money"]: 3933 info.extend([ 3934 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3935 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3936 ]) 3937 3938 else: 3939 info.append("\nNo withdrawal limits\n") 3940 3941 for curr in view["limits"]["money"].keys(): 3942 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3943 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3944 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3945 3946 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3947 "[{}]".format(curr), 3948 "{:.2f}".format(view["limits"]["money"][curr]), 3949 "{:.2f}".format(availableMoney), 3950 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3951 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3952 ) 3953 3954 if curr == "rub": 3955 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3956 3957 else: 3958 info.append(infoStr) 3959 3960 infoText = "".join(info) 3961 3962 uLogger.info(infoText) 3963 3964 if self.withdrawalLimitsFile: 3965 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3966 fH.write(infoText) 3967 3968 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3969 3970 if self.useHTMLReports: 3971 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3972 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3973 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3974 3975 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3976 3977 return view 3978 3979 def RequestAccounts(self) -> dict: 3980 """ 3981 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3982 3983 See also: 3984 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3985 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3986 - `OverviewUserInfo()` method 3987 3988 :return: dict with raw data from server that contains accounts info. Example of dict: 3989 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3990 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3991 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3992 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3993 """ 3994 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3995 3996 self.body = str({}) 3997 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3998 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3999 4000 if self.moreDebug: 4001 uLogger.debug("Records about available accounts successfully received") 4002 4003 return rawAccounts 4004 4005 def RequestUserInfo(self) -> dict: 4006 """ 4007 Method for requesting common user's information. 4008 4009 See also: 4010 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4011 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4012 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4013 - `OverviewUserInfo()` method 4014 4015 :return: dict with raw data from server that contains user's information. Example of dict: 4016 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4017 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4018 """ 4019 uLogger.debug("Requesting common user's information. Wait, please...") 4020 4021 self.body = str({}) 4022 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4023 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4024 4025 if self.moreDebug: 4026 uLogger.debug("Records about current user successfully received") 4027 4028 return rawUserInfo 4029 4030 def RequestMarginStatus(self, accountId: str = None) -> dict: 4031 """ 4032 Method for requesting margin calculation for defined account ID. 4033 4034 See also: 4035 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4036 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4037 - `OverviewUserInfo()` method 4038 4039 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4040 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4041 Example of responses: 4042 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4043 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4044 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4045 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4046 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4047 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4048 """ 4049 if accountId is None or not accountId: 4050 if self.accountId is None or not self.accountId: 4051 uLogger.error("Variable `accountId` must be defined for using this method!") 4052 raise Exception("Account ID required") 4053 4054 else: 4055 accountId = self.accountId # use `self.accountId` (main ID) by default 4056 4057 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4058 4059 self.body = str({"accountId": accountId}) 4060 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4061 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4062 4063 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4064 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4065 rawMargin = {} 4066 4067 else: 4068 if self.moreDebug: 4069 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4070 4071 return rawMargin 4072 4073 def RequestTariffLimits(self) -> dict: 4074 """ 4075 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4076 4077 See also: 4078 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4079 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4080 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4081 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4082 - `OverviewUserInfo()` method 4083 4084 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4085 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4086 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4087 """ 4088 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4089 4090 self.body = str({}) 4091 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4092 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4093 4094 if self.moreDebug: 4095 uLogger.debug("Records with limits of current tariff successfully received") 4096 4097 return rawTariffLimits 4098 4099 def RequestBondCoupons(self, iJSON: dict) -> dict: 4100 """ 4101 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4102 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4103 All dates are in UTC timezone. 4104 4105 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4106 Documentation: 4107 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4108 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4109 4110 See also: `ExtendBondsData()`. 4111 4112 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4113 If raw iJSON is not data of bond then server returns an error [400] with message: 4114 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4115 :return: dictionary with bond payment calendar. Response example 4116 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4117 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4118 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4119 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4120 """ 4121 if iJSON["figi"] is None or not iJSON["figi"]: 4122 uLogger.error("FIGI must be defined for using this method!") 4123 raise Exception("FIGI required") 4124 4125 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4126 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4127 4128 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4129 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4130 self._figi, 4131 startDate, 4132 endDate, 4133 )) 4134 4135 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4136 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4137 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4138 4139 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4140 uLogger.warning("Instrument type is not bond!") 4141 4142 else: 4143 if self.moreDebug: 4144 uLogger.debug("Records about bond payment calendar successfully received") 4145 4146 return calendar 4147 4148 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4149 """ 4150 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4151 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4152 coupon yields, current yields and some statistics etc. 4153 4154 WARNING! This is too long operation if a lot of bonds requested from broker server. 4155 4156 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4157 4158 :param instruments: list of strings with tickers or FIGIs. 4159 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4160 for further used by data scientists or stock analytics. 4161 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4162 In XLSX-file and Pandas DataFrame fields mean: 4163 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4164 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4165 """ 4166 if instruments is None or not instruments: 4167 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4168 raise Exception("Ticker or FIGI required") 4169 4170 if isinstance(instruments, str): 4171 instruments = [instruments] 4172 4173 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4174 4175 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4176 4177 iCount = len(uniqueInstruments) 4178 tooLong = iCount >= 20 4179 if tooLong: 4180 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4181 4182 bonds = None 4183 for i, self._figi in enumerate(uniqueInstruments): 4184 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4185 4186 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4187 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4188 rawBond = self.SearchByFIGI(requestPrice=True) 4189 4190 # Widen raw data with UTC current time (iData["actualDateTime"]): 4191 actualDate = datetime.now(tzutc()) 4192 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4193 4194 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4195 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4196 4197 # Replace some values with human-readable: 4198 iData["nominalCurrency"] = iData["nominal"]["currency"] 4199 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4200 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4201 iData["aciCurrency"] = iData["aciValue"]["currency"] 4202 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4203 iData["issueSize"] = int(iData["issueSize"]) 4204 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4205 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4206 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4207 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4208 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4209 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4210 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4211 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4212 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4213 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4214 4215 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4216 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4217 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4218 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4219 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4220 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4221 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4222 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4223 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4224 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4225 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4226 4227 # Widen raw data with calendar data from `rawCalendar` values: 4228 calendarData = [] 4229 if "events" in iData["rawCalendar"].keys(): 4230 for item in iData["rawCalendar"]["events"]: 4231 calendarData.append({ 4232 "couponDate": item["couponDate"], 4233 "couponNumber": int(item["couponNumber"]), 4234 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4235 "payCurrency": item["payOneBond"]["currency"], 4236 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4237 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4238 "couponStartDate": item["couponStartDate"], 4239 "couponEndDate": item["couponEndDate"], 4240 "couponPeriod": item["couponPeriod"], 4241 }) 4242 4243 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4244 if "maturityDate" not in iData.keys(): 4245 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4246 4247 # Widen raw data with Coupon Rate. 4248 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4249 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4250 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4251 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4252 4253 # Widen raw data with Yield to Maturity (YTM) on current date. 4254 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4255 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4256 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4257 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4258 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4259 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4260 4261 iData["calendar"] = calendarData # adds calendar at the end 4262 4263 # Remove not used data: 4264 iData.pop("uid") 4265 iData.pop("positionUid") 4266 iData.pop("currentPrice") 4267 iData.pop("rawCalendar") 4268 4269 colNames = list(iData.keys()) 4270 if bonds is None: 4271 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4272 4273 else: 4274 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4275 4276 else: 4277 uLogger.warning("Instrument is not a bond!") 4278 4279 processed = round(100 * (i + 1) / iCount, 1) 4280 if tooLong and processed % 5 == 0: 4281 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4282 4283 else: 4284 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4285 4286 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4287 4288 # Saving bonds from Pandas DataFrame to XLSX sheet: 4289 if xlsx and self.bondsXLSXFile: 4290 with pd.ExcelWriter( 4291 path=self.bondsXLSXFile, 4292 date_format=TKS_DATE_FORMAT, 4293 datetime_format=TKS_DATE_TIME_FORMAT, 4294 mode="w", 4295 ) as writer: 4296 bonds.to_excel( 4297 writer, 4298 sheet_name="Extended bonds data", 4299 index=True, 4300 encoding="UTF-8", 4301 freeze_panes=(1, 1), 4302 ) # saving as XLSX-file with freeze first row and column as headers 4303 4304 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4305 4306 return bonds 4307 4308 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4309 """ 4310 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4311 4312 WARNING! This is too long operation if a lot of bonds requested from broker server. 4313 4314 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4315 4316 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4317 extended information about bonds: main info, current prices, bond payment calendar, 4318 coupon yields, current yields and some statistics etc. 4319 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4320 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4321 for further used by data scientists or stock analytics. 4322 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4323 """ 4324 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4325 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4326 4327 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4328 4329 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4330 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4331 calendar = None 4332 for bond in extBonds.iterrows(): 4333 for item in bond[1]["calendar"]: 4334 cData = { 4335 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4336 "couponDate": item["couponDate"], 4337 "figi": bond[1]["figi"], 4338 "ticker": bond[1]["ticker"], 4339 "name": bond[1]["name"], 4340 "couponNumber": item["couponNumber"], 4341 "payOneBond": item["payOneBond"], 4342 "payCurrency": item["payCurrency"], 4343 "couponType": item["couponType"], 4344 "couponPeriod": item["couponPeriod"], 4345 "fixDate": item["fixDate"], 4346 "couponStartDate": item["couponStartDate"], 4347 "couponEndDate": item["couponEndDate"], 4348 } 4349 4350 if calendar is None: 4351 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4352 4353 else: 4354 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4355 4356 if calendar is not None: 4357 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4358 4359 # Saving calendar from Pandas DataFrame to XLSX sheet: 4360 if xlsx: 4361 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4362 4363 with pd.ExcelWriter( 4364 path=xlsxCalendarFile, 4365 date_format=TKS_DATE_FORMAT, 4366 datetime_format=TKS_DATE_TIME_FORMAT, 4367 mode="w", 4368 ) as writer: 4369 humanReadable = calendar.copy(deep=True) 4370 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4371 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4372 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4373 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4374 humanReadable.columns = colNames # human-readable column names 4375 4376 humanReadable.to_excel( 4377 writer, 4378 sheet_name="Bond payments calendar", 4379 index=False, 4380 encoding="UTF-8", 4381 freeze_panes=(1, 2), 4382 ) # saving as XLSX-file with freeze first row and column as headers 4383 4384 del humanReadable # release df in memory 4385 4386 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4387 4388 return calendar 4389 4390 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4391 """ 4392 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4393 Also, creates Markdown file with calendar data, `calendar.md` by default. 4394 4395 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4396 4397 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4398 extended information about bonds: main info, current prices, bond payment calendar, 4399 coupon yields, current yields and some statistics etc. 4400 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4401 :param show: if `True` then also printing bonds payment calendar to the console, 4402 otherwise save to file `calendarFile` only. `False` by default. 4403 :return: multilines text in Markdown format with bonds payment calendar as a table. 4404 """ 4405 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4406 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4407 4408 infoText = "# Bond payments calendar\n\n" 4409 4410 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4411 4412 if not (calendar is None or calendar.empty): 4413 splitLine = "| | | | | | | | | |\n" 4414 4415 info = [ 4416 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4417 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4418 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4419 ] 4420 4421 newMonth = False 4422 notOneBond = calendar["figi"].nunique() > 1 4423 for i, bond in enumerate(calendar.iterrows()): 4424 if newMonth and notOneBond: 4425 info.append(splitLine) 4426 4427 info.append( 4428 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4429 " √" if bond[1]["paid"] else " —", 4430 bond[1]["couponDate"].split("T")[0], 4431 bond[1]["figi"], 4432 bond[1]["ticker"], 4433 bond[1]["couponNumber"], 4434 "{} {}".format( 4435 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4436 bond[1]["payCurrency"], 4437 ), 4438 bond[1]["couponType"], 4439 bond[1]["couponPeriod"], 4440 bond[1]["fixDate"].split("T")[0], 4441 ) 4442 ) 4443 4444 if i < len(calendar.values) - 1: 4445 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4446 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4447 newMonth = False if curDate.month == nextDate.month else True 4448 4449 else: 4450 newMonth = False 4451 4452 infoText += "".join(info) 4453 4454 if show: 4455 uLogger.info("{}".format(infoText)) 4456 4457 if self.calendarFile is not None: 4458 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4459 fH.write(infoText) 4460 4461 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4462 4463 if self.useHTMLReports: 4464 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4465 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4466 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4467 4468 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4469 4470 else: 4471 infoText += "No data\n" 4472 4473 return infoText 4474 4475 def OverviewAccounts(self, show: bool = False) -> dict: 4476 """ 4477 Method for parsing and show simple table with all available user accounts. 4478 4479 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4480 4481 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4482 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4483 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4484 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4485 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4486 "closed": "—", "access": "Full access" }, ...}}` 4487 """ 4488 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4489 4490 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4491 accounts = { 4492 item["id"]: { 4493 "type": TKS_ACCOUNT_TYPES[item["type"]], 4494 "name": item["name"], 4495 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4496 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4497 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4498 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4499 } for item in rawAccounts["accounts"] 4500 } 4501 4502 # Raw and parsed data with some fields replaced in "stat" section: 4503 view = { 4504 "rawAccounts": rawAccounts, 4505 "stat": accounts, 4506 } 4507 4508 # --- Prepare simple text table with only accounts data in human-readable format: 4509 if show: 4510 info = [ 4511 "# User accounts\n\n", 4512 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4513 "| Account ID | Type | Status | Name |\n", 4514 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4515 ] 4516 4517 for account in view["stat"].keys(): 4518 info.extend([ 4519 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4520 account, 4521 view["stat"][account]["type"], 4522 view["stat"][account]["status"], 4523 view["stat"][account]["name"], 4524 ) 4525 ]) 4526 4527 infoText = "".join(info) 4528 4529 uLogger.info(infoText) 4530 4531 if self.userAccountsFile: 4532 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4533 fH.write(infoText) 4534 4535 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4536 4537 if self.useHTMLReports: 4538 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4539 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4540 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4541 4542 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4543 4544 return view 4545 4546 def OverviewUserInfo(self, show: bool = False) -> dict: 4547 """ 4548 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4549 4550 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4551 4552 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4553 :return: dict with raw parsed data from server and some calculated statistics about it. 4554 """ 4555 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4556 tmpTicker = self._ticker 4557 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4558 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4559 self._ticker = tmpTicker 4560 4561 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4562 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4563 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4564 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4565 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4566 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4567 4568 # This is dict with parsed common user data: 4569 userInfo = { 4570 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4571 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4572 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4573 "tariff": rawUserInfo["tariff"], 4574 } 4575 4576 # This is an array of dict with parsed margin statuses for every account IDs: 4577 margins = {} 4578 for accountId in accounts.keys(): 4579 if rawMargins[accountId]: 4580 margins[accountId] = { 4581 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4582 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4583 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4584 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4585 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4586 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4587 "missing": missing["volume"], 4588 } 4589 4590 else: 4591 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4592 4593 unary = {} # unary-connection limits 4594 for item in rawTariffLimits["unaryLimits"]: 4595 if item["limitPerMinute"] in unary.keys(): 4596 unary[item["limitPerMinute"]].extend(item["methods"]) 4597 4598 else: 4599 unary[item["limitPerMinute"]] = item["methods"] 4600 4601 stream = {} # stream-connection limits 4602 for item in rawTariffLimits["streamLimits"]: 4603 if item["limit"] in stream.keys(): 4604 stream[item["limit"]].extend(item["streams"]) 4605 4606 else: 4607 stream[item["limit"]] = item["streams"] 4608 4609 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4610 limits = { 4611 "unary": unary, 4612 "stream": stream, 4613 } 4614 4615 # Raw and parsed data as an output result: 4616 view = { 4617 "rawUserInfo": rawUserInfo, 4618 "rawAccounts": rawAccounts, 4619 "rawMargins": rawMargins, 4620 "rawTariffLimits": rawTariffLimits, 4621 "stat": { 4622 "overview": overview, 4623 "userInfo": userInfo, 4624 "accounts": accounts, 4625 "margins": margins, 4626 "limits": limits, 4627 }, 4628 } 4629 4630 # --- Prepare text table with user information in human-readable format: 4631 if show: 4632 info = [ 4633 "# Full user information\n\n", 4634 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4635 "## Common information\n\n", 4636 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4637 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4638 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4639 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4640 "\n## User accounts\n\n", 4641 ] 4642 4643 for account in view["stat"]["accounts"].keys(): 4644 info.extend([ 4645 "### ID: [{}]\n\n".format(account), 4646 "| Parameters | Values |\n", 4647 "|----------------------|--------------------------------------------------------------|\n", 4648 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4649 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4650 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4651 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4652 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4653 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4654 ]) 4655 4656 if margins[account]: 4657 info.extend([ 4658 "| Margin status: | Enabled |\n", 4659 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4660 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4661 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4662 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4663 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4664 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4665 ]) 4666 4667 else: 4668 info.append("| Margin status: | Disabled |\n\n") 4669 4670 info.extend([ 4671 "\n## Current user tariff limits\n", 4672 "\n### See also\n", 4673 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4674 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4675 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4676 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4677 "\n### Unary limits\n", 4678 ]) 4679 4680 if unary: 4681 for key, values in sorted(unary.items()): 4682 info.append("\n* Max requests per minute: {}\n".format(key)) 4683 4684 for value in values: 4685 info.append(" - {}\n".format(value)) 4686 4687 else: 4688 info.append("\nNot available\n") 4689 4690 info.append("\n### Stream limits\n") 4691 4692 if stream: 4693 for key, values in sorted(stream.items()): 4694 info.append("\n* Max stream connections: {}\n".format(key)) 4695 4696 for value in values: 4697 info.append(" - {}\n".format(value)) 4698 4699 else: 4700 info.append("\nNot available\n") 4701 4702 infoText = "".join(info) 4703 4704 uLogger.info(infoText) 4705 4706 if self.userInfoFile: 4707 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4708 fH.write(infoText) 4709 4710 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4711 4712 if self.useHTMLReports: 4713 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4714 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4715 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4716 4717 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4718 4719 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self.__lock = Lock() # initialize multiprocessing mutex lock 130 131 self.aliases = TKS_TICKER_ALIASES 132 """Some aliases instead official tickers. 133 134 See also: `TKSEnums.TKS_TICKER_ALIASES` 135 """ 136 137 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 138 139 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 140 141 self._ticker = "" 142 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 143 144 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 145 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 146 147 See also: `SearchByTicker()`, `SearchInstruments()`. 148 """ 149 150 self._figi = "" 151 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 152 153 See also: `SearchByFIGI()`, `SearchInstruments()`. 154 """ 155 156 self.depth = 1 157 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 158 159 See also: `GetCurrentPrices()`. 160 """ 161 162 self.server = r"https://invest-public-api.tinkoff.ru/rest" 163 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 164 165 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 166 """ 167 168 uLogger.debug("Broker API server: {}".format(self.server)) 169 170 self.timeout = 15 171 """Server operations timeout in seconds. Default: `15`. 172 173 See also: `SendAPIRequest()`. 174 """ 175 176 self.headers = { 177 "Content-Type": "application/json", 178 "accept": "application/json", 179 "Authorization": "Bearer {}".format(self.token), 180 "x-app-name": "Tim55667757.TKSBrokerAPI", 181 } 182 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 183 184 See also: `SendAPIRequest()`. 185 """ 186 187 self.body = None 188 """Request body which send to broker server. Default: `None`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.moreDebug = False 194 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 195 196 self.useHTMLReports = False 197 """ 198 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 199 200 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 201 """ 202 203 self.historyFile = None 204 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 205 206 See also: `History()`. 207 """ 208 209 self.htmlHistoryFile = "index.html" 210 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 211 212 See also: `ShowHistoryChart()`. 213 """ 214 215 self.instrumentsFile = "instruments.md" 216 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 217 218 See also: `ShowInstrumentsInfo()`. 219 """ 220 221 self.searchResultsFile = "search-results.md" 222 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 223 224 See also: `SearchInstruments()`. 225 """ 226 227 self.pricesFile = "prices.md" 228 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 229 230 See also: `GetListOfPrices()`. 231 """ 232 233 self.infoFile = "info.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 237 """ 238 239 self.bondsXLSXFile = "ext-bonds.xlsx" 240 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 241 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 242 243 See also: `ExtendBondsData()`. 244 """ 245 246 self.calendarFile = "calendar.md" 247 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 248 249 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 250 251 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 252 """ 253 254 self.overviewFile = "overview.md" 255 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 256 257 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 258 """ 259 260 self.overviewDigestFile = "overview-digest.md" 261 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 262 263 See also: `Overview()` with parameter `details="digest"`. 264 """ 265 266 self.overviewPositionsFile = "overview-positions.md" 267 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 268 269 See also: `Overview()` with parameter `details="positions"`. 270 """ 271 272 self.overviewOrdersFile = "overview-orders.md" 273 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 274 275 See also: `Overview()` with parameter `details="orders"`. 276 """ 277 278 self.overviewAnalyticsFile = "overview-analytics.md" 279 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 280 281 See also: `Overview()` with parameter `details="analytics"`. 282 """ 283 284 self.overviewBondsCalendarFile = "overview-calendar.md" 285 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 286 287 See also: `Overview()` with parameter `details="calendar"`. 288 """ 289 290 self.reportFile = "deals.md" 291 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 292 293 See also: `Deals()`. 294 """ 295 296 self.withdrawalLimitsFile = "limits.md" 297 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 298 299 See also: `OverviewLimits()` and `RequestLimits()`. 300 """ 301 302 self.userInfoFile = "user-info.md" 303 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 304 305 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 306 """ 307 308 self.userAccountsFile = "accounts.md" 309 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 310 311 See also: `OverviewAccounts()`, `RequestAccounts()`. 312 """ 313 314 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 315 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 316 317 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 318 319 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 320 """ 321 322 self.iList = None # init iList for raw instruments data 323 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 324 325 See also: `Listing()`, `DumpInstruments()`. 326 """ 327 328 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 329 if useCache: 330 if os.path.exists(self.iListDumpFile): 331 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 332 curTime = datetime.now(tzutc()) 333 334 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 335 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 336 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 338 339 else: 340 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 341 342 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 343 os.path.abspath(self.iListDumpFile), 344 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 345 )) 346 347 else: 348 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 349 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 350 351 else: 352 self.iList = self.Listing() # request new raw instruments data from broker server 353 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 354 355 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 356 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 357 358 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 359 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
413 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 414 """ 415 Send GET or POST request to broker server and receive JSON object. 416 417 self.header: must be defining with dictionary of headers. 418 self.body: if define then used as request body. None by default. 419 self.timeout: global request timeout, 15 seconds by default. 420 :param url: url with REST request. 421 :param reqType: send "GET" or "POST" request. "GET" by default. 422 :param retry: how many times retry after first request if an 5xx server errors occurred. 423 :param pause: sleep time in seconds between retries. 424 :return: response JSON (dictionary) from broker. 425 """ 426 if reqType.upper() not in ("GET", "POST"): 427 uLogger.error("You can define request type: `GET` or `POST`!") 428 raise Exception("Incorrect value") 429 430 if self.moreDebug: 431 uLogger.debug("Request parameters:") 432 uLogger.debug(" - REST API URL: {}".format(url)) 433 uLogger.debug(" - request type: {}".format(reqType)) 434 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 435 uLogger.debug(" - body:\n{}".format(self.body)) 436 437 # fast hack to avoid all operations with some tickers/FIGI 438 responseJSON = {} 439 oK = True 440 for item in self.exclude: 441 if item in url: 442 if self.moreDebug: 443 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 444 445 oK = False 446 break 447 448 if oK: 449 with self.__lock: # acquire the mutex lock 450 counter = 0 451 response = None 452 errMsg = "" 453 454 while not response and counter <= retry: 455 if reqType == "GET": 456 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 457 458 if reqType == "POST": 459 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 460 461 if self.moreDebug: 462 uLogger.debug("Response:") 463 uLogger.debug(" - status code: {}".format(response.status_code)) 464 uLogger.debug(" - reason: {}".format(response.reason)) 465 uLogger.debug(" - body length: {}".format(len(response.text))) 466 uLogger.debug(" - headers:\n{}".format(response.headers)) 467 468 # Server returns some headers: 469 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 470 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 471 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 472 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 473 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 474 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 475 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 476 sleep(rateLimitWait) 477 478 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 479 if 400 <= response.status_code < 500: 480 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 481 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 482 483 if "code" in response.text and "message" in response.text: 484 msgDict = self._ParseJSON(rawData=response.text) 485 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 486 487 counter = retry + 1 # do not retry for 4xx errors 488 489 if 500 <= response.status_code < 600: 490 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 491 uLogger.debug(" - not oK, {}".format(errMsg)) 492 493 if "code" in response.text and "message" in response.text: 494 errMsgDict = self._ParseJSON(rawData=response.text) 495 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 496 497 counter += 1 498 499 if counter <= retry: 500 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 501 sleep(pause) 502 503 responseJSON = self._ParseJSON(rawData=response.text) 504 505 if errMsg: 506 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 507 uLogger.error(" - not oK, {}".format(errMsg)) 508 509 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
542 def Listing(self) -> dict: 543 """ 544 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 545 546 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 547 """ 548 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 549 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 550 551 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 552 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 554 555 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 556 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 557 poolUpdater.close() # close the thread pool 558 poolUpdater.join() # wait a moment until all data returns from threads 559 560 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 561 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 562 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 563 564 # calculate minimum price increment (step) for all instruments and set up instrument's type: 565 for iType in iList.keys(): 566 for ticker in iList[iType]: 567 iList[iType][ticker]["type"] = iType 568 569 if "minPriceIncrement" in iList[iType][ticker].keys(): 570 iList[iType][ticker]["step"] = NanoToFloat( 571 iList[iType][ticker]["minPriceIncrement"]["units"], 572 iList[iType][ticker]["minPriceIncrement"]["nano"], 573 ) 574 575 else: 576 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 577 578 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
580 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 581 """ 582 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 583 584 See also: `DumpInstruments()`, `Listing()`. 585 586 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 587 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 588 """ 589 if self.iListDumpFile is None or not self.iListDumpFile: 590 uLogger.error("Output name of dump file must be defined!") 591 raise Exception("Filename required") 592 593 if not self.iList or forceUpdate: 594 self.iList = self.Listing() 595 596 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 597 598 # Save as XLSX with separated sheets for every type of instruments: 599 with pd.ExcelWriter( 600 path=xlsxDumpFile, 601 date_format=TKS_DATE_FORMAT, 602 datetime_format=TKS_DATE_TIME_FORMAT, 603 mode="w", 604 ) as writer: 605 for iType in TKS_INSTRUMENTS: 606 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 607 df = df[sorted(df)] # sorted by column names 608 df = df.applymap( 609 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 610 na_action="ignore", 611 ) # converting numbers from nano-type to float in every cell 612 df.to_excel( 613 writer, 614 sheet_name=iType, 615 encoding="UTF-8", 616 freeze_panes=(1, 1), 617 ) # saving as XLSX-file with freeze first row and column as headers 618 619 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
621 def DumpInstruments(self, forceUpdate: bool = True) -> str: 622 """ 623 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 624 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 625 626 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 627 628 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 629 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 630 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 640 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 641 fH.write(jsonDump) 642 643 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 644 645 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
647 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 648 """ 649 Show information about one instrument defined by json data and prints it in Markdown format. 650 651 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 652 653 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 654 :param show: if `True` then also printing information about instrument and its current price. 655 :return: multilines text in Markdown format with information about one instrument. 656 """ 657 splitLine = "| | |\n" 658 infoText = "" 659 660 if iJSON is not None and iJSON and isinstance(iJSON, dict): 661 info = [ 662 "# Main information\n\n", 663 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 664 "| Parameters | Values |\n", 665 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 666 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 667 "| Full name: | {:<54} |\n".format(iJSON["name"]), 668 ] 669 670 if "sector" in iJSON.keys() and iJSON["sector"]: 671 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 672 673 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 674 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 675 676 info.extend([ 677 splitLine, 678 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 679 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 680 ]) 681 682 if "isin" in iJSON.keys() and iJSON["isin"]: 683 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 684 685 if "classCode" in iJSON.keys(): 686 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 687 688 info.extend([ 689 splitLine, 690 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 691 splitLine, 692 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 693 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 694 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 695 ]) 696 697 if iJSON["figi"]: 698 self._figi = iJSON["figi"] 699 iJSON = iJSON | self.RequestTradingStatus() 700 701 info.extend([ 702 splitLine, 703 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 704 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 705 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 706 ]) 707 708 info.append(splitLine) 709 710 if "type" in iJSON.keys() and iJSON["type"]: 711 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 712 713 if "shareType" in iJSON.keys() and iJSON["shareType"]: 714 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 715 716 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 717 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 718 719 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 720 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 721 722 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 723 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 724 725 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 726 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 727 728 if "focusType" in iJSON.keys() and iJSON["focusType"]: 729 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 730 731 if "assetType" in iJSON.keys() and iJSON["assetType"]: 732 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 733 734 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 735 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 736 737 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 738 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 739 740 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 741 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 742 743 if "currency" in iJSON.keys(): 744 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 745 746 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 747 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 748 749 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 750 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 751 752 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 753 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 754 755 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 756 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 757 758 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 759 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 760 761 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 762 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 763 764 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 765 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 766 767 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 768 info.append("| Perpetual bond: | Yes |\n") 769 770 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 771 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 772 773 iExt = None 774 if iJSON["type"] == "Bonds": 775 info.extend([ 776 splitLine, 777 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 778 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 779 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 780 iJSON["nominal"]["currency"], 781 )), 782 ]) 783 784 if "floatingCouponFlag" in iJSON.keys(): 785 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 786 787 if "amortizationFlag" in iJSON.keys(): 788 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 789 790 info.append(splitLine) 791 792 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 793 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 794 795 if iJSON["figi"]: 796 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 797 798 info.extend([ 799 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 800 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 801 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 802 ]) 803 804 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 805 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 806 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 807 iJSON["aciValue"]["currency"] 808 ))) 809 810 if "currentPrice" in iJSON.keys(): 811 info.append(splitLine) 812 813 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 814 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 815 816 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 817 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 818 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 819 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 820 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 821 822 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 823 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 824 825 info.extend([ 826 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 827 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 828 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 829 )), 830 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 831 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 832 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 833 )), 834 "| Changes between last deal price and last close | {:<54} |\n".format( 835 "{:.2f}%{}".format( 836 iJSON["currentPrice"]["changes"], 837 " ({}{:.2f} {})".format( 838 "+" if bondChangesDelta > 0 else "", 839 bondChangesDelta, 840 aciCurrency 841 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 842 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 843 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 844 currency 845 ), 846 ) 847 ), 848 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 849 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 850 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 851 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 852 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 853 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 854 )), 855 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 856 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 857 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 858 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 859 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 860 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 861 )), 862 ]) 863 864 if "lot" in iJSON.keys(): 865 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 866 867 if "step" in iJSON.keys() and iJSON["step"] != 0: 868 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 869 870 # Add bond payment calendar: 871 if iJSON["type"] == "Bonds": 872 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 873 info.extend(["\n#", strCalendar]) 874 875 infoText += "".join(info) 876 877 if show: 878 uLogger.info("{}".format(infoText)) 879 880 else: 881 uLogger.debug("{}".format(infoText)) 882 883 if self.infoFile is not None: 884 with open(self.infoFile, "w", encoding="UTF-8") as fH: 885 fH.write(infoText) 886 887 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 888 889 if self.useHTMLReports: 890 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 891 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 892 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 893 894 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 895 896 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
898 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 tickerJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 909 910 if not self._ticker: 911 uLogger.warning("self._ticker variable is not be empty!") 912 913 else: 914 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 if self._ticker in self.iList["Shares"].keys(): 922 tickerJSON = self.iList["Shares"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 925 926 elif self._ticker in self.iList["Currencies"].keys(): 927 tickerJSON = self.iList["Currencies"][self._ticker] 928 if self.moreDebug: 929 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 930 931 elif self._ticker in self.iList["Bonds"].keys(): 932 tickerJSON = self.iList["Bonds"][self._ticker] 933 if self.moreDebug: 934 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 935 936 elif self._ticker in self.iList["Etfs"].keys(): 937 tickerJSON = self.iList["Etfs"][self._ticker] 938 if self.moreDebug: 939 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 940 941 elif self._ticker in self.iList["Futures"].keys(): 942 tickerJSON = self.iList["Futures"][self._ticker] 943 if self.moreDebug: 944 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 945 946 if tickerJSON: 947 self._figi = tickerJSON["figi"] 948 949 if requestPrice: 950 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 951 952 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 953 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 954 955 else: 956 tickerJSON["currentPrice"]["changes"] = 0 957 958 if show: 959 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 960 961 else: 962 if show: 963 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 964 965 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
967 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 968 """ 969 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 970 971 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 972 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 973 :return: JSON formatted data with information about instrument. 974 """ 975 figiJSON = {} 976 if self.moreDebug: 977 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 978 979 if not self._figi: 980 uLogger.warning("self._figi variable is not be empty!") 981 982 else: 983 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 984 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 985 raise Exception("Instrument not allowed") 986 987 if not self.iList: 988 self.iList = self.Listing() 989 990 for item in self.iList["Shares"].keys(): 991 if self._figi == self.iList["Shares"][item]["figi"]: 992 figiJSON = self.iList["Shares"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Currencies"].keys(): 1001 if self._figi == self.iList["Currencies"][item]["figi"]: 1002 figiJSON = self.iList["Currencies"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Bonds"].keys(): 1011 if self._figi == self.iList["Bonds"][item]["figi"]: 1012 figiJSON = self.iList["Bonds"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1016 1017 break 1018 1019 if not figiJSON: 1020 for item in self.iList["Etfs"].keys(): 1021 if self._figi == self.iList["Etfs"][item]["figi"]: 1022 figiJSON = self.iList["Etfs"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Futures"].keys(): 1031 if self._figi == self.iList["Futures"][item]["figi"]: 1032 figiJSON = self.iList["Futures"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1036 1037 break 1038 1039 if figiJSON: 1040 self._figi = figiJSON["figi"] 1041 self._ticker = figiJSON["ticker"] 1042 1043 if requestPrice: 1044 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1045 1046 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1047 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1048 1049 else: 1050 figiJSON["currentPrice"]["changes"] = 0 1051 1052 if show: 1053 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1054 1055 else: 1056 if show: 1057 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1058 1059 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1061 def GetCurrentPrices(self, show: bool = True) -> dict: 1062 """ 1063 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1064 `{"buy": [{"price": 1243.8, "quantity": 193}, 1065 {"price": 1244.0, "quantity": 168}, 1066 {"price": 1244.8, "quantity": 5}, 1067 {"price": 1245.0, "quantity": 61}, 1068 {"price": 1245.4, "quantity": 60}], 1069 "sell": [{"price": 1243.6, "quantity": 8}, 1070 {"price": 1242.6, "quantity": 10}, 1071 {"price": 1242.4, "quantity": 18}, 1072 {"price": 1242.2, "quantity": 50}, 1073 {"price": 1242.0, "quantity": 113}], 1074 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1075 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1076 - sell: list of dicts with Buyers prices, 1077 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1078 - quantity: volume value by current price in lots, 1079 - limitUp: current trade session limit price, maximum, 1080 - limitDown: current trade session limit price, minimum, 1081 - lastPrice: last deal price of the instrument, 1082 - closePrice: previous trade session close price of the instrument. 1083 1084 See also: `SearchByTicker()` and `SearchByFIGI()`. 1085 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1086 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1087 1088 :param show: if `True` then print DOM to log and console. 1089 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1090 If an error occurred then returns an empty record: 1091 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1092 """ 1093 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1094 1095 if self.depth < 1: 1096 uLogger.error("Depth of Market (DOM) must be >=1!") 1097 raise Exception("Incorrect value") 1098 1099 if not (self._ticker or self._figi): 1100 uLogger.error("self._ticker or self._figi variables must be defined!") 1101 raise Exception("Ticker or FIGI required") 1102 1103 if self._ticker and not self._figi: 1104 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1105 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1106 1107 if not self._ticker and self._figi: 1108 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1109 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1110 1111 if not self._figi: 1112 uLogger.error("FIGI is not defined!") 1113 raise Exception("Ticker or FIGI required") 1114 1115 else: 1116 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1117 1118 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1120 self.body = str({"figi": self._figi, "depth": self.depth}) 1121 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1124 # list of dicts with sellers orders: 1125 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1126 1127 # list of dicts with buyers orders: 1128 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1129 1130 # max price of instrument at this time: 1131 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1132 1133 # min price of instrument at this time: 1134 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1135 1136 # last price of deal with instrument: 1137 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1138 1139 # last close price of instrument: 1140 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1141 1142 else: 1143 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1144 uLogger.debug("Server response: {}".format(pricesResponse)) 1145 1146 if show: 1147 if prices["buy"] or prices["sell"]: 1148 info = [ 1149 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1150 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1151 self._ticker, 1152 self._figi, 1153 self.depth, 1154 ), 1155 "-" * 60, "\n", 1156 " Orders of Buyers | Orders of Sellers\n", 1157 "-" * 60, "\n", 1158 " Sell prices (volumes) | Buy prices (volumes)\n", 1159 "-" * 60, "\n", 1160 ] 1161 1162 if not prices["buy"]: 1163 info.append(" | No orders!\n") 1164 sumBuy = 0 1165 1166 else: 1167 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1168 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1169 for item in maxMinSorted: 1170 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1171 1172 if not prices["sell"]: 1173 info.append("No orders! |\n") 1174 sumSell = 0 1175 1176 else: 1177 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1178 for item in prices["sell"]: 1179 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1180 1181 info.extend([ 1182 "-" * 60, "\n", 1183 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1184 "-" * 60, "\n", 1185 ]) 1186 1187 infoText = "".join(info) 1188 1189 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1190 1191 else: 1192 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1193 1194 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1196 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1197 """ 1198 This method get and show information about all available broker instruments for current user account. 1199 If `instrumentsFile` string is not empty then also save information to this file. 1200 1201 :param show: if `True` then print results to console, if `False` — print only to file. 1202 :return: multi-lines string with all available broker instruments 1203 """ 1204 if not self.iList: 1205 self.iList = self.Listing() 1206 1207 info = [ 1208 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1209 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1210 ] 1211 1212 # add instruments count by type: 1213 for iType in self.iList.keys(): 1214 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1215 1216 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1217 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1218 1219 # generating info tables with all instruments by type: 1220 for iType in self.iList.keys(): 1221 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1222 1223 for instrument in self.iList[iType].keys(): 1224 iName = self.iList[iType][instrument]["name"] # instrument's name 1225 if len(iName) > 57: 1226 iName = "{}...".format(iName[:54]) # right trim for a long string 1227 1228 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1229 self.iList[iType][instrument]["ticker"], 1230 iName, 1231 self.iList[iType][instrument]["figi"], 1232 self.iList[iType][instrument]["currency"], 1233 self.iList[iType][instrument]["lot"], 1234 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1235 )) 1236 1237 infoText = "".join(info) 1238 1239 if show: 1240 uLogger.info(infoText) 1241 1242 if self.instrumentsFile: 1243 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1244 fH.write(infoText) 1245 1246 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1247 1248 if self.useHTMLReports: 1249 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1250 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1251 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1252 1253 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1254 1255 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1257 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1258 """ 1259 This method search and show information about instruments by part of its ticker, FIGI or name. 1260 If `searchResultsFile` string is not empty then also save information to this file. 1261 1262 :param pattern: string with part of ticker, FIGI or instrument's name. 1263 :param show: if `True` then print results to console, if `False` — return list of result only. 1264 :return: list of dictionaries with all found instruments. 1265 """ 1266 if not self.iList: 1267 self.iList = self.Listing() 1268 1269 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1270 compiledPattern = re.compile(pattern, re.IGNORECASE) 1271 1272 for iType in self.iList: 1273 for instrument in self.iList[iType].values(): 1274 searchResult = compiledPattern.search(" ".join( 1275 [instrument["ticker"], instrument["figi"], instrument["name"]] 1276 )) 1277 1278 if searchResult: 1279 searchResults[iType][instrument["ticker"]] = instrument 1280 1281 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1282 info = [ 1283 "# Search results\n\n", 1284 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1285 "* **Search pattern:** [{}]\n".format(pattern), 1286 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1287 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1288 ] 1289 infoShort = info[:] 1290 1291 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1292 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1293 skippedLine = "| ... | ... | ... | ... |\n" 1294 1295 if resultsLen == 0: 1296 info.append("\nNo results\n") 1297 infoShort.append("\nNo results\n") 1298 uLogger.warning("No results. Try changing your search pattern.") 1299 1300 else: 1301 for iType in searchResults: 1302 iTypeValuesCount = len(searchResults[iType].values()) 1303 if iTypeValuesCount > 0: 1304 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1305 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1306 1307 for instrument in searchResults[iType].values(): 1308 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1309 instrument["type"], 1310 instrument["ticker"], 1311 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1312 instrument["figi"], 1313 )) 1314 1315 if iTypeValuesCount <= 5: 1316 infoShort.extend(info[-iTypeValuesCount:]) 1317 1318 else: 1319 infoShort.extend(info[-5:]) 1320 infoShort.append(skippedLine) 1321 1322 infoText = "".join(info) 1323 infoTextShort = "".join(infoShort) 1324 1325 if show: 1326 uLogger.info(infoTextShort) 1327 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1328 1329 if self.searchResultsFile: 1330 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1331 fH.write(infoText) 1332 1333 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1334 1335 if self.useHTMLReports: 1336 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1337 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1338 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1339 1340 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1341 1342 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1344 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1345 """ 1346 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1347 1348 :param instruments: list of strings with tickers or FIGIs. 1349 :return: list with unique instrument FIGIs only. 1350 """ 1351 requestedInstruments = [] 1352 for iName in instruments: 1353 if iName not in self.aliases.keys(): 1354 if iName not in requestedInstruments: 1355 requestedInstruments.append(iName) 1356 1357 else: 1358 if iName not in requestedInstruments: 1359 if self.aliases[iName] not in requestedInstruments: 1360 requestedInstruments.append(self.aliases[iName]) 1361 1362 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1363 1364 onlyUniqueFIGIs = [] 1365 for iName in requestedInstruments: 1366 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1367 continue 1368 1369 self._ticker = iName 1370 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1371 1372 if not iData: 1373 self._ticker = "" 1374 self._figi = iName 1375 1376 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1377 1378 if not iData: 1379 self._figi = "" 1380 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1381 1382 if iData and iData["figi"] not in onlyUniqueFIGIs: 1383 onlyUniqueFIGIs.append(iData["figi"]) 1384 1385 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1386 1387 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1389 def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]: 1390 """ 1391 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1392 1393 See limits: https://tinkoff.github.io/investAPI/limits/ 1394 1395 If `pricesFile` string is not empty then also save information to this file. 1396 1397 :param instruments: list of strings with tickers or FIGIs. 1398 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1399 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1400 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1401 """ 1402 if instruments is None or not instruments: 1403 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1404 raise Exception("Ticker or FIGI required") 1405 1406 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1407 1408 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1409 1410 iList = [] # trying to get info and current prices about all unique instruments: 1411 for self._figi in onlyUniqueFIGIs: 1412 iData = self.SearchByFIGI(requestPrice=True) 1413 iList.append(iData) 1414 1415 self.ShowListOfPrices(iList, show) 1416 1417 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1419 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1420 """ 1421 Show table contains current prices of given instruments. 1422 1423 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1424 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: multilines text in Markdown format as a table contains current prices. 1427 """ 1428 infoText = "" 1429 1430 if show or self.pricesFile: 1431 info = [ 1432 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1433 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1434 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1435 ] 1436 1437 for item in iList: 1438 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1439 item["ticker"], 1440 item["figi"], 1441 item["type"], 1442 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1443 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1444 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1445 "{} / {}".format( 1446 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1447 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1448 ), 1449 "{} / {}".format( 1450 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1451 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1452 ), 1453 item["currency"], 1454 )) 1455 1456 infoText = "".join(info) 1457 1458 if show: 1459 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1460 1461 if self.pricesFile: 1462 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1463 fH.write(infoText) 1464 1465 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1466 1467 if self.useHTMLReports: 1468 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1469 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1470 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1471 1472 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1473 1474 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1476 def RequestTradingStatus(self) -> dict: 1477 """ 1478 Requesting trading status for the instrument defined by `figi` variable. 1479 1480 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1481 1482 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1483 1484 :return: dictionary with trading status attributes. Response example: 1485 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1486 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1487 """ 1488 if self._figi is None or not self._figi: 1489 uLogger.error("Variable `figi` must be defined for using this method!") 1490 raise Exception("FIGI required") 1491 1492 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1493 1494 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1495 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1496 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1497 1498 if self.moreDebug: 1499 uLogger.debug("Records about current trading status successfully received") 1500 1501 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1503 def RequestPortfolio(self) -> dict: 1504 """ 1505 Requesting actual user's portfolio for current `accountId`. 1506 1507 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1508 1509 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1510 1511 :return: dictionary with user's portfolio. 1512 """ 1513 if self.accountId is None or not self.accountId: 1514 uLogger.error("Variable `accountId` must be defined for using this method!") 1515 raise Exception("Account ID required") 1516 1517 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1518 1519 self.body = str({"accountId": self.accountId}) 1520 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1521 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1522 1523 if self.moreDebug: 1524 uLogger.debug("Records about user's portfolio successfully received") 1525 1526 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1528 def RequestPositions(self) -> dict: 1529 """ 1530 Requesting open positions by currencies and instruments for current `accountId`. 1531 1532 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1533 1534 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1535 1536 :return: dictionary with open positions by instruments. 1537 """ 1538 if self.accountId is None or not self.accountId: 1539 uLogger.error("Variable `accountId` must be defined for using this method!") 1540 raise Exception("Account ID required") 1541 1542 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1543 1544 self.body = str({"accountId": self.accountId}) 1545 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1546 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1547 1548 if self.moreDebug: 1549 uLogger.debug("Records about current open positions successfully received") 1550 1551 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1553 def RequestPendingOrders(self) -> list: 1554 """ 1555 Requesting current actual pending limit orders for current `accountId`. 1556 1557 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1558 1559 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1560 1561 :return: list of dictionaries with pending limit orders. 1562 """ 1563 if self.accountId is None or not self.accountId: 1564 uLogger.error("Variable `accountId` must be defined for using this method!") 1565 raise Exception("Account ID required") 1566 1567 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1568 1569 self.body = str({"accountId": self.accountId}) 1570 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1571 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1572 1573 if "orders" in rawResponse.keys(): 1574 rawOrders = rawResponse["orders"] 1575 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1576 1577 else: 1578 rawOrders = [] 1579 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1580 1581 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1583 def RequestStopOrders(self) -> list: 1584 """ 1585 Requesting current actual stop orders for current `accountId`. 1586 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1602 1603 if "stopOrders" in rawResponse.keys(): 1604 rawStopOrders = rawResponse["stopOrders"] 1605 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1606 1607 else: 1608 rawStopOrders = [] 1609 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1610 1611 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1613 def Overview(self, show: bool = False, details: str = "full") -> dict: 1614 """ 1615 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1616 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1617 and `overviewBondsCalendarFile` are defined then also save information to file. 1618 1619 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1620 many requests about the state of the portfolio, and then, based on the received data, a large number 1621 of calculation and statistics are collected. 1622 1623 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1624 :param details: how detailed should the information be? 1625 - `full` — shows full available information about portfolio status (by default), 1626 - `positions` — shows only open positions, 1627 - `orders` — shows only sections of open limits and stop orders. 1628 - `digest` — show a short digest of the portfolio status, 1629 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1630 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1631 :return: dictionary with client's raw portfolio and some statistics. 1632 """ 1633 if self.accountId is None or not self.accountId: 1634 uLogger.error("Variable `accountId` must be defined for using this method!") 1635 raise Exception("Account ID required") 1636 1637 view = { 1638 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1639 "headers": {}, # list of dictionaries, response headers without "positions" section 1640 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1641 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1642 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1643 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1644 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1645 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1646 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1647 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1648 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1649 }, 1650 "stat": { # --- some statistics calculated using "raw" sections: 1651 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1652 "availableRUB": 0., # available rubles (without other currencies) 1653 "blockedRUB": 0., # blocked sum in Russian Rouble 1654 "totalChangesRUB": 0., # changes for all open trades in RUB 1655 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1656 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1657 "sharesCostRUB": 0., # costs of all shares in RUB 1658 "bondsCostRUB": 0., # costs of all bonds in RUB 1659 "etfsCostRUB": 0., # costs of all etfs in RUB 1660 "futuresCostRUB": 0., # costs of all futures in RUB 1661 "Currencies": [], # list of dictionaries of all currencies statistics 1662 "Shares": [], # list of dictionaries of all shares statistics 1663 "Bonds": [], # list of dictionaries of all bonds statistics 1664 "Etfs": [], # list of dictionaries of all etfs statistics 1665 "Futures": [], # list of dictionaries of all futures statistics 1666 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1667 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1668 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1669 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1670 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1671 }, 1672 "analytics": { # --- some analytics of portfolio: 1673 "distrByAssets": {}, # portfolio distribution by assets 1674 "distrByCompanies": {}, # portfolio distribution by companies 1675 "distrBySectors": {}, # portfolio distribution by sectors 1676 "distrByCurrencies": {}, # portfolio distribution by currencies 1677 "distrByCountries": {}, # portfolio distribution by countries 1678 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1679 } 1680 } 1681 1682 details = details.lower() 1683 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1684 if details not in availableDetails: 1685 details = "full" 1686 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1687 1688 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1689 1690 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1691 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1692 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1693 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1694 1695 # save response headers without "positions" section: 1696 for key in portfolioResponse.keys(): 1697 if key != "positions": 1698 view["raw"]["headers"][key] = portfolioResponse[key] 1699 1700 else: 1701 continue 1702 1703 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1704 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1705 for item in portfolioResponse["positions"]: 1706 if item["instrumentType"] == "currency": 1707 self._figi = item["figi"] 1708 curr = self.SearchByFIGI(requestPrice=False) 1709 1710 # current price of currency in RUB: 1711 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1712 "name": curr["name"], 1713 "currentPrice": NanoToFloat( 1714 item["currentPrice"]["units"], 1715 item["currentPrice"]["nano"] 1716 ), 1717 } 1718 1719 view["raw"]["Currencies"].append(item) 1720 1721 elif item["instrumentType"] == "share": 1722 view["raw"]["Shares"].append(item) 1723 1724 elif item["instrumentType"] == "bond": 1725 view["raw"]["Bonds"].append(item) 1726 1727 elif item["instrumentType"] == "etf": 1728 view["raw"]["Etfs"].append(item) 1729 1730 elif item["instrumentType"] == "futures": 1731 view["raw"]["Futures"].append(item) 1732 1733 else: 1734 continue 1735 1736 # how many volume of currencies (by ISO currency name) are blocked: 1737 for item in view["raw"]["positions"]["blocked"]: 1738 blocked = NanoToFloat(item["units"], item["nano"]) 1739 if blocked > 0: 1740 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1741 1742 # how many volume of instruments (by FIGI) are blocked: 1743 for item in view["raw"]["positions"]["securities"]: 1744 blocked = int(item["blocked"]) 1745 if blocked > 0: 1746 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1747 1748 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1749 1750 if "rub" in allBlocked.keys(): 1751 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1752 1753 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1754 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1755 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1756 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1757 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1758 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1759 view["stat"]["portfolioCostRUB"] = sum([ 1760 view["stat"]["allCurrenciesCostRUB"], 1761 view["stat"]["sharesCostRUB"], 1762 view["stat"]["bondsCostRUB"], 1763 view["stat"]["etfsCostRUB"], 1764 view["stat"]["futuresCostRUB"], 1765 ]) 1766 1767 # --- calculating some portfolio statistics: 1768 byComp = {} # distribution by companies 1769 bySect = {} # distribution by sectors 1770 byCurr = {} # distribution by currencies (include RUB) 1771 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1772 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1773 1774 for item in portfolioResponse["positions"]: 1775 self._figi = item["figi"] 1776 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1777 1778 if instrument: 1779 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1780 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1781 1782 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1783 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1784 1785 else: 1786 blocked = 0 1787 1788 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1789 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1790 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1791 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1792 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1793 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1794 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1795 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1796 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1797 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1798 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1799 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1800 1801 statData = { 1802 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1803 "ticker": instrument["ticker"], # ticker by FIGI 1804 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1805 "volume": volume, # available volume of instrument 1806 "lots": lots, # volume in lots of instrument 1807 "direction": direction, # direction of an instrument's position: short or long 1808 "blocked": blocked, # blocked volume of currency or instrument 1809 "currentPrice": curPrice, # current instrument's price in basic asset 1810 "average": average, # current average position price 1811 "cost": cost, # current cost of all volume of instrument in basic asset 1812 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1813 "costRUB": costRUB, # cost of instrument in ruble 1814 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1815 "profit": profit, # expected profit at current moment 1816 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1817 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1818 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1819 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1820 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1821 "step": instrument["step"], # minimum price increment 1822 } 1823 1824 # adding distribution by unique countries: 1825 if statData["country"] not in byCountry.keys(): 1826 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1827 1828 else: 1829 byCountry[statData["country"]]["cost"] += costRUB 1830 byCountry[statData["country"]]["percent"] += percentCostRUB 1831 1832 if item["instrumentType"] != "currency": 1833 # adding distribution by unique companies: 1834 if statData["name"]: 1835 if statData["name"] not in byComp.keys(): 1836 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 byComp[statData["name"]]["cost"] += costRUB 1840 byComp[statData["name"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique sectors: 1843 if statData["sector"] not in bySect.keys(): 1844 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 bySect[statData["sector"]]["cost"] += costRUB 1848 bySect[statData["sector"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique currencies: 1851 if currency not in byCurr.keys(): 1852 byCurr[currency] = { 1853 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1854 "cost": costRUB, 1855 "percent": percentCostRUB 1856 } 1857 1858 else: 1859 byCurr[currency]["cost"] += costRUB 1860 byCurr[currency]["percent"] += percentCostRUB 1861 1862 # saving statistics for every instrument: 1863 if item["instrumentType"] == "currency": 1864 view["stat"]["Currencies"].append(statData) 1865 1866 # update dict with free funds for trading (total - blocked) by currencies 1867 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1868 view["stat"]["funds"][currency] = { 1869 "total": volume, 1870 "totalCostRUB": costRUB, # total volume cost in rubles 1871 "free": volume - blocked, 1872 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1873 } 1874 1875 elif item["instrumentType"] == "share": 1876 view["stat"]["Shares"].append(statData) 1877 1878 elif item["instrumentType"] == "bond": 1879 view["stat"]["Bonds"].append(statData) 1880 1881 elif item["instrumentType"] == "etf": 1882 view["stat"]["Etfs"].append(statData) 1883 1884 elif item["instrumentType"] == "Futures": 1885 view["stat"]["Futures"].append(statData) 1886 1887 else: 1888 continue 1889 1890 # total changes in Russian Ruble: 1891 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1892 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1893 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1894 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1895 view["stat"]["funds"]["rub"] = { 1896 "total": view["stat"]["availableRUB"], 1897 "totalCostRUB": view["stat"]["availableRUB"], 1898 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1899 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1900 } 1901 1902 # --- pending limit orders sector data: 1903 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1904 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1905 1906 for item in view["raw"]["orders"]: 1907 self._figi = item["figi"] 1908 1909 if item["figi"] not in uniquePendingOrdersFIGIs: 1910 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1911 1912 uniquePendingOrdersFIGIs.append(item["figi"]) 1913 uniquePendingOrders[item["figi"]] = instrument 1914 1915 else: 1916 instrument = uniquePendingOrders[item["figi"]] 1917 1918 if instrument: 1919 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1920 orderType = TKS_ORDER_TYPES[item["orderType"]] 1921 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1922 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1923 1924 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1925 if item["direction"] == "ORDER_DIRECTION_BUY": 1926 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1927 1928 else: 1929 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1930 1931 # requested price for order execution: 1932 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1933 1934 # necessary changes in percent to reach target from current price: 1935 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1936 1937 view["stat"]["orders"].append({ 1938 "orderID": item["orderId"], # orderId number parameter of current order 1939 "figi": item["figi"], # FIGI identification 1940 "ticker": instrument["ticker"], # ticker name by FIGI 1941 "lotsRequested": item["lotsRequested"], # requested lots value 1942 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1943 "currentPrice": lastPrice, # current instrument's price for defined action 1944 "targetPrice": target, # requested price for order execution in base currency 1945 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1946 "percentChanges": changes, # changes in percent to target from current price 1947 "currency": item["currency"], # instrument's currency name 1948 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1949 "type": orderType, # type of order from TKS_ORDER_TYPES 1950 "status": orderState, # order status from TKS_ORDER_STATES 1951 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1952 }) 1953 1954 # --- stop orders sector data: 1955 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1956 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1957 1958 for item in view["raw"]["stopOrders"]: 1959 self._figi = item["figi"] 1960 1961 if item["figi"] not in uniqueStopOrdersFIGIs: 1962 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1963 1964 uniqueStopOrdersFIGIs.append(item["figi"]) 1965 uniqueStopOrders[item["figi"]] = instrument 1966 1967 else: 1968 instrument = uniqueStopOrders[item["figi"]] 1969 1970 if instrument: 1971 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1972 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1973 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1974 1975 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1976 if "expirationTime" in item.keys(): 1977 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1978 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1979 1980 else: 1981 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1982 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1983 1984 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1985 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1986 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1987 1988 else: 1989 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1990 1991 # requested price when stop-order executed: 1992 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1993 1994 # price for limit-order, set up when stop-order executed: 1995 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1996 1997 # necessary changes in percent to reach target from current price: 1998 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1999 2000 view["stat"]["stopOrders"].append({ 2001 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2002 "figi": item["figi"], # FIGI identification 2003 "ticker": instrument["ticker"], # ticker name by FIGI 2004 "lotsRequested": item["lotsRequested"], # requested lots value 2005 "currentPrice": lastPrice, # current instrument's price for defined action 2006 "targetPrice": target, # requested price for stop-order execution in base currency 2007 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2008 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2009 "percentChanges": changes, # changes in percent to target from current price 2010 "currency": item["currency"], # instrument's currency name 2011 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2012 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2013 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2014 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2015 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2016 }) 2017 2018 # --- calculating data for analytics section: 2019 # portfolio distribution by assets: 2020 view["analytics"]["distrByAssets"] = { 2021 "Ruble": { 2022 "uniques": 1, 2023 "cost": view["stat"]["availableRUB"], 2024 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 }, 2026 "Currencies": { 2027 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2028 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2029 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2030 }, 2031 "Shares": { 2032 "uniques": len(view["stat"]["Shares"]), 2033 "cost": view["stat"]["sharesCostRUB"], 2034 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2035 }, 2036 "Bonds": { 2037 "uniques": len(view["stat"]["Bonds"]), 2038 "cost": view["stat"]["bondsCostRUB"], 2039 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2040 }, 2041 "Etfs": { 2042 "uniques": len(view["stat"]["Etfs"]), 2043 "cost": view["stat"]["etfsCostRUB"], 2044 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2045 }, 2046 "Futures": { 2047 "uniques": len(view["stat"]["Futures"]), 2048 "cost": view["stat"]["futuresCostRUB"], 2049 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2050 }, 2051 } 2052 2053 # portfolio distribution by companies: 2054 view["analytics"]["distrByCompanies"]["All money cash"] = { 2055 "ticker": "", 2056 "cost": view["stat"]["allCurrenciesCostRUB"], 2057 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 } 2059 view["analytics"]["distrByCompanies"].update(byComp) 2060 2061 # portfolio distribution by sectors: 2062 view["analytics"]["distrBySectors"]["All money cash"] = { 2063 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2064 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2065 } 2066 view["analytics"]["distrBySectors"].update(bySect) 2067 2068 # portfolio distribution by currencies: 2069 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2070 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2071 2072 if self.moreDebug: 2073 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2074 2075 view["analytics"]["distrByCurrencies"].update(byCurr) 2076 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2077 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2078 2079 # portfolio distribution by countries: 2080 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2081 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2082 2083 if self.moreDebug: 2084 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2085 2086 view["analytics"]["distrByCountries"].update(byCountry) 2087 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2088 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2089 2090 # --- Prepare text statistics overview in human-readable: 2091 if show: 2092 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2093 2094 # Whatever the value `details`, header not changes: 2095 info = [ 2096 "# Client's portfolio\n\n", 2097 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2098 "* **Account ID:** [{}]\n".format(self.accountId), 2099 ] 2100 2101 if details in ["full", "positions", "digest"]: 2102 info.extend([ 2103 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2104 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2105 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2106 view["stat"]["totalChangesRUB"], 2107 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2108 view["stat"]["totalChangesPercentRUB"], 2109 ), 2110 ]) 2111 2112 if details in ["full", "positions"]: 2113 info.extend([ 2114 "## Open positions\n\n", 2115 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2116 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2117 "| **Ruble:** | {:>31} | | | | | |\n".format( 2118 "{:.2f} ({:.2f}) rub".format( 2119 view["stat"]["availableRUB"], 2120 view["stat"]["blockedRUB"], 2121 ) 2122 ) 2123 ]) 2124 2125 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2126 return [ 2127 "| | | | | | | |\n", 2128 "| {:<27} | | | | | {:>19} | |\n".format( 2129 noTradeStr if noTradeStr else typeStr, 2130 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2131 ), 2132 ] 2133 2134 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2135 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2136 "{} [{}]".format(data["ticker"], data["figi"]), 2137 "{:.2f} ({:.2f}) {}".format( 2138 data["volume"], 2139 data["blocked"], 2140 data["currency"], 2141 ) if isCurr else "{:.0f} ({:.0f})".format( 2142 data["volume"], 2143 data["blocked"], 2144 ), 2145 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2146 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2147 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2148 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2149 "{}{:.2f} {} ({}{:.2f}%)".format( 2150 "+" if data["profit"] > 0 else "", 2151 data["profit"], data["baseCurrencyName"], 2152 "+" if data["percentProfit"] > 0 else "", 2153 data["percentProfit"], 2154 ), 2155 ) 2156 2157 # --- Show currencies section: 2158 if view["stat"]["Currencies"]: 2159 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2160 for item in view["stat"]["Currencies"]: 2161 info.append(_InfoStr(item, isCurr=True)) 2162 2163 else: 2164 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2165 2166 # --- Show shares section: 2167 if view["stat"]["Shares"]: 2168 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2169 2170 for item in view["stat"]["Shares"]: 2171 info.append(_InfoStr(item)) 2172 2173 else: 2174 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2175 2176 # --- Show bonds section: 2177 if view["stat"]["Bonds"]: 2178 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2179 2180 for item in view["stat"]["Bonds"]: 2181 info.append(_InfoStr(item)) 2182 2183 else: 2184 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2185 2186 # --- Show etfs section: 2187 if view["stat"]["Etfs"]: 2188 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2189 2190 for item in view["stat"]["Etfs"]: 2191 info.append(_InfoStr(item)) 2192 2193 else: 2194 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2195 2196 # --- Show futures section: 2197 if view["stat"]["Futures"]: 2198 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2199 2200 for item in view["stat"]["Futures"]: 2201 info.append(_InfoStr(item)) 2202 2203 else: 2204 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2205 2206 if details in ["full", "orders"]: 2207 # --- Show pending limit orders section: 2208 if view["stat"]["orders"]: 2209 info.extend([ 2210 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2211 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2212 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2213 ]) 2214 2215 for item in view["stat"]["orders"]: 2216 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2217 "{} [{}]".format(item["ticker"], item["figi"]), 2218 item["orderID"], 2219 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2220 "{} {} ({}{:.2f}%)".format( 2221 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2222 item["baseCurrencyName"], 2223 "+" if item["percentChanges"] > 0 else "", 2224 float(item["percentChanges"]), 2225 ), 2226 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2227 item["action"], 2228 item["type"], 2229 item["date"], 2230 )) 2231 2232 else: 2233 info.append("\n## Total pending limit-orders: [0]\n") 2234 2235 # --- Show stop orders section: 2236 if view["stat"]["stopOrders"]: 2237 info.extend([ 2238 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2239 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2240 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2241 ]) 2242 2243 for item in view["stat"]["stopOrders"]: 2244 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2245 "{} [{}]".format(item["ticker"], item["figi"]), 2246 item["orderID"], 2247 item["lotsRequested"], 2248 "{} {} ({}{:.2f}%)".format( 2249 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2250 item["baseCurrencyName"], 2251 "+" if item["percentChanges"] > 0 else "", 2252 float(item["percentChanges"]), 2253 ), 2254 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2255 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2256 item["action"], 2257 item["type"], 2258 item["expType"], 2259 item["createDate"], 2260 item["expDate"], 2261 )) 2262 2263 else: 2264 info.append("\n## Total stop-orders: [0]\n") 2265 2266 if details in ["full", "analytics"]: 2267 # -- Show analytics section: 2268 if view["stat"]["portfolioCostRUB"] > 0: 2269 info.extend([ 2270 "\n# Analytics\n\n" 2271 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2272 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2273 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2274 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2275 view["stat"]["totalChangesRUB"], 2276 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2277 view["stat"]["totalChangesPercentRUB"], 2278 ), 2279 "\n## Portfolio distribution by assets\n" 2280 "\n| Type | Uniques | Percent | Current cost |\n", 2281 "|------------------------------------|---------|---------|--------------------|\n", 2282 ]) 2283 2284 for key in view["analytics"]["distrByAssets"].keys(): 2285 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2286 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2287 key, 2288 view["analytics"]["distrByAssets"][key]["uniques"], 2289 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2290 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2291 )) 2292 2293 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2294 2295 info.extend([ 2296 "\n## Portfolio distribution by companies\n" 2297 "\n| Company | Percent | Current cost |\n", 2298 aSepLine, 2299 ]) 2300 2301 for company in view["analytics"]["distrByCompanies"].keys(): 2302 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2303 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2304 "{}{}".format( 2305 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2306 company, 2307 ), 2308 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2309 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2310 )) 2311 2312 info.extend([ 2313 "\n## Portfolio distribution by sectors\n" 2314 "\n| Sector | Percent | Current cost |\n", 2315 aSepLine, 2316 ]) 2317 2318 for sector in view["analytics"]["distrBySectors"].keys(): 2319 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2320 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2321 sector, 2322 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2324 )) 2325 2326 info.extend([ 2327 "\n## Portfolio distribution by currencies\n" 2328 "\n| Instruments currencies | Percent | Current cost |\n", 2329 aSepLine, 2330 ]) 2331 2332 for curr in view["analytics"]["distrByCurrencies"].keys(): 2333 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2334 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2335 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2336 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2338 )) 2339 2340 info.extend([ 2341 "\n## Portfolio distribution by countries\n" 2342 "\n| Assets by country | Percent | Current cost |\n", 2343 aSepLine, 2344 ]) 2345 2346 for country in view["analytics"]["distrByCountries"].keys(): 2347 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2348 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2349 country, 2350 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2351 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2352 )) 2353 2354 if details in ["full", "calendar"]: 2355 # -- Show bonds payment calendar section: 2356 if view["stat"]["Bonds"]: 2357 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2358 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2359 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2360 2361 else: 2362 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2363 2364 infoText = "".join(info) 2365 2366 uLogger.info(infoText) 2367 2368 if details == "full" and self.overviewFile: 2369 filename = self.overviewFile 2370 2371 elif details == "digest" and self.overviewDigestFile: 2372 filename = self.overviewDigestFile 2373 2374 elif details == "positions" and self.overviewPositionsFile: 2375 filename = self.overviewPositionsFile 2376 2377 elif details == "orders" and self.overviewOrdersFile: 2378 filename = self.overviewOrdersFile 2379 2380 elif details == "analytics" and self.overviewAnalyticsFile: 2381 filename = self.overviewAnalyticsFile 2382 2383 elif details == "calendar" and self.overviewBondsCalendarFile: 2384 filename = self.overviewBondsCalendarFile 2385 2386 else: 2387 filename = "" 2388 2389 if filename: 2390 with open(filename, "w", encoding="UTF-8") as fH: 2391 fH.write(infoText) 2392 2393 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2394 2395 if self.useHTMLReports: 2396 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2397 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2398 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="", commonCSS=COMMON_CSS, markdown=infoText)) 2399 2400 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2401 2402 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2404 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2405 """ 2406 Returns history operations between two given dates for current `accountId`. 2407 If `reportFile` string is not empty then also save human-readable report. 2408 Shows some statistical data of closed positions. 2409 2410 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2411 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2412 :param show: if `True` then also prints all records to the console. 2413 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2414 :return: original list of dictionaries with history of deals records from API ("operations" key): 2415 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2416 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2417 """ 2418 if self.accountId is None or not self.accountId: 2419 uLogger.error("Variable `accountId` must be defined for using this method!") 2420 raise Exception("Account ID required") 2421 2422 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2423 2424 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2425 2426 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2427 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2428 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2429 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2430 customStat = {} # custom statistics in additional to responseJSON 2431 2432 # --- output report in human-readable format: 2433 if show or self.reportFile: 2434 splitLine1 = "| | | | | |\n" # Summary section 2435 splitLine2 = "| | | | | | | | |\n" # Operations section 2436 nextDay = "" 2437 2438 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2439 2440 if len(ops) > 0: 2441 customStat = { 2442 "opsCount": 0, # total operations count 2443 "buyCount": 0, # buy operations 2444 "sellCount": 0, # sell operations 2445 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2446 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2447 "payIn": {"rub": 0.}, # Deposit brokerage account 2448 "payOut": {"rub": 0.}, # Withdrawals 2449 "divs": {"rub": 0.}, # Dividends income 2450 "coupons": {"rub": 0.}, # Coupon's income 2451 "brokerCom": {"rub": 0.}, # Service commissions 2452 "serviceCom": {"rub": 0.}, # Service commissions 2453 "marginCom": {"rub": 0.}, # Margin commissions 2454 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2455 } 2456 2457 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2458 for item in ops: 2459 if item["state"] == "OPERATION_STATE_EXECUTED": 2460 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2461 2462 # count buy operations: 2463 if "_BUY" in item["operationType"]: 2464 customStat["buyCount"] += 1 2465 2466 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2467 customStat["buyTotal"][item["payment"]["currency"]] += payment 2468 2469 else: 2470 customStat["buyTotal"][item["payment"]["currency"]] = payment 2471 2472 # count sell operations: 2473 elif "_SELL" in item["operationType"]: 2474 customStat["sellCount"] += 1 2475 2476 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2477 customStat["sellTotal"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["sellTotal"][item["payment"]["currency"]] = payment 2481 2482 # count incoming operations: 2483 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2484 if item["payment"]["currency"] in customStat["payIn"].keys(): 2485 customStat["payIn"][item["payment"]["currency"]] += payment 2486 2487 else: 2488 customStat["payIn"][item["payment"]["currency"]] = payment 2489 2490 # count withdrawals operations: 2491 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2492 if item["payment"]["currency"] in customStat["payOut"].keys(): 2493 customStat["payOut"][item["payment"]["currency"]] += payment 2494 2495 else: 2496 customStat["payOut"][item["payment"]["currency"]] = payment 2497 2498 # count dividends income: 2499 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2500 if item["payment"]["currency"] in customStat["divs"].keys(): 2501 customStat["divs"][item["payment"]["currency"]] += payment 2502 2503 else: 2504 customStat["divs"][item["payment"]["currency"]] = payment 2505 2506 # count coupon's income: 2507 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2508 if item["payment"]["currency"] in customStat["coupons"].keys(): 2509 customStat["coupons"][item["payment"]["currency"]] += payment 2510 2511 else: 2512 customStat["coupons"][item["payment"]["currency"]] = payment 2513 2514 # count broker commissions: 2515 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2516 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2517 customStat["brokerCom"][item["payment"]["currency"]] += payment 2518 2519 else: 2520 customStat["brokerCom"][item["payment"]["currency"]] = payment 2521 2522 # count service commissions: 2523 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2524 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2525 customStat["serviceCom"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["serviceCom"][item["payment"]["currency"]] = payment 2529 2530 # count margin commissions: 2531 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2532 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2533 customStat["marginCom"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["marginCom"][item["payment"]["currency"]] = payment 2537 2538 # count withholding taxes: 2539 elif "_TAX" in item["operationType"]: 2540 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2541 customStat["allTaxes"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["allTaxes"][item["payment"]["currency"]] = payment 2545 2546 else: 2547 continue 2548 2549 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2550 2551 # --- view "Actions" lines: 2552 info.extend([ 2553 "| Report sections | | | | |\n", 2554 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2555 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2556 "| | Buy: {:<22} | {:<28} | | |\n".format( 2557 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2558 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2559 ), 2560 "| | Sell: {:<21} | {:<28} | | |\n".format( 2561 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2562 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2563 ), 2564 ]) 2565 2566 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2567 for key in opsKeys: 2568 if key == "rub": 2569 continue 2570 2571 info.extend([ 2572 "| | | {:<28} | | |\n".format( 2573 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2574 ), 2575 "| | | {:<28} | | |\n".format( 2576 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2577 ), 2578 ]) 2579 2580 info.append(splitLine1) 2581 2582 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2583 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2584 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2586 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2587 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2588 ) 2589 2590 # --- view "Payments" lines: 2591 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2592 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2593 2594 for key in paymentsKeys: 2595 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2596 2597 info.append(splitLine1) 2598 2599 # --- view "Commissions and taxes" lines: 2600 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2601 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2602 2603 for key in comKeys: 2604 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2605 2606 info.extend([ 2607 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2608 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2609 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2610 ]) 2611 2612 else: 2613 info.append("Broker returned no operations during this period\n") 2614 2615 # --- view "Operations" section: 2616 for item in ops: 2617 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2618 continue 2619 2620 else: 2621 self._figi = item["figi"] if item["figi"] else "" 2622 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2623 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2624 2625 # group of deals during one day: 2626 if nextDay and item["date"].split("T")[0] != nextDay: 2627 info.append(splitLine2) 2628 nextDay = "" 2629 2630 else: 2631 nextDay = item["date"].split("T")[0] # saving current day for splitting 2632 2633 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2634 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2635 self._figi if self._figi else "—", 2636 instrument["ticker"] if instrument else "—", 2637 instrument["type"] if instrument else "—", 2638 item["quantity"] if int(item["quantity"]) > 0 else "—", 2639 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2640 TKS_OPERATION_STATES[item["state"]], 2641 TKS_OPERATION_TYPES[item["operationType"]], 2642 )) 2643 2644 infoText = "".join(info) 2645 2646 if show: 2647 if self.moreDebug: 2648 uLogger.debug("Records about history of a client's operations successfully received") 2649 2650 uLogger.info(infoText) 2651 2652 if self.reportFile: 2653 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2654 fH.write(infoText) 2655 2656 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2657 2658 if self.useHTMLReports: 2659 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2660 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2661 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2662 2663 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2664 2665 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2667 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2668 """ 2669 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2670 2671 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2672 Warning! Broker server used ISO UTC time by default. 2673 2674 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2675 Also, `historyFile` used to update history with `onlyMissing` parameter. 2676 2677 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2678 2679 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2680 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2681 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2682 `"hour"`, `"day"`. Default: `"hour"`. 2683 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2684 False by default. Warning! History appends only from last candle to current time 2685 with always update last candle! 2686 :param csvSep: separator if csv-file is used, `,` by default. 2687 :param show: if `True` then also prints Pandas DataFrame to the console. 2688 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2689 `["date", "time", "open", "high", "low", "close", "volume"]`. 2690 """ 2691 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2692 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2693 history = None # empty pandas object for history 2694 2695 if interval not in TKS_CANDLE_INTERVALS.keys(): 2696 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2697 raise Exception("Incorrect value") 2698 2699 if not (self._ticker or self._figi): 2700 uLogger.error("Ticker or FIGI must be defined!") 2701 raise Exception("Ticker or FIGI required") 2702 2703 if self._ticker and not self._figi: 2704 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2705 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2706 2707 if self._figi and not self._ticker: 2708 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2709 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2710 2711 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2712 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2713 if interval.lower() != "day": 2714 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2715 2716 delta = dtEnd - dtStart # current UTC time minus last time in file 2717 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2718 2719 # calculate history length in candles: 2720 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2721 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2722 length += 1 # to avoid fraction time 2723 2724 # calculate data blocks count: 2725 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2726 2727 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2728 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2729 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2730 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2731 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2732 2733 tempOld = None # pandas object for old history, if --only-missing key present 2734 lastTime = None # datetime object of last old candle in file 2735 2736 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2737 uLogger.debug("--only-missing key present, add only last missing candles...") 2738 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2739 2740 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2741 2742 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2743 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2744 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2745 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2746 2747 # get last datetime object from last string in file or minus 1 delta if file is empty: 2748 if len(tempOld) > 0: 2749 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2750 2751 else: 2752 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2753 2754 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2755 2756 responseJSONs = [] # raw history blocks of data 2757 2758 blockEnd = dtEnd 2759 for item in range(blocks): 2760 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2761 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2762 2763 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2764 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2765 )) 2766 2767 if blockStart == blockEnd: 2768 uLogger.debug("Skipped this zero-length block...") 2769 2770 else: 2771 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2772 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2773 self.body = str({ 2774 "figi": self._figi, 2775 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2776 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2777 "interval": TKS_CANDLE_INTERVALS[interval][0] 2778 }) 2779 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2780 2781 if "code" in responseJSON.keys(): 2782 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2783 2784 else: 2785 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2786 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2787 2788 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2789 2790 blockEnd = blockStart 2791 2792 printCount = len(responseJSONs) # candles to show in console 2793 if responseJSONs: 2794 tempHistory = pd.DataFrame( 2795 data={ 2796 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2797 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2798 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2799 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2800 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2801 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2802 "volume": [int(item["volume"]) for item in responseJSONs], 2803 }, 2804 index=range(len(responseJSONs)), 2805 columns=["date", "time", "open", "high", "low", "close", "volume"], 2806 ) 2807 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2808 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2809 2810 # append only newest candles to old history if --only-missing key present: 2811 if onlyMissing and tempOld is not None and lastTime is not None: 2812 index = 0 # find start index in tempHistory data: 2813 2814 for i, item in tempHistory.iterrows(): 2815 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2816 2817 if curTime == lastTime: 2818 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2819 index = i 2820 printCount = index + 1 2821 break 2822 2823 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2824 2825 else: 2826 history = tempHistory # if no `--only-missing` key then load full data from server 2827 2828 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2829 2830 if history is not None and not history.empty: 2831 if show: 2832 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2833 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2834 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2835 )) 2836 2837 else: 2838 uLogger.warning("Received an empty candles history!") 2839 2840 if self.historyFile is not None: 2841 if history is not None and not history.empty: 2842 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2843 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2844 2845 else: 2846 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2847 2848 else: 2849 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2850 2851 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2853 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2854 """ 2855 Load candles history from csv-file and return Pandas DataFrame object. 2856 2857 See also: `History()` and `ShowHistoryChart()` methods. 2858 2859 :param filePath: path to csv-file to open. 2860 """ 2861 loadedHistory = None # init candles data object 2862 2863 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2864 2865 if os.path.exists(filePath): 2866 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2867 2868 tfStr = self.priceModel.FormattedDelta( 2869 self.priceModel.timeframe, 2870 "{days} days {hours}h {minutes}m {seconds}s", 2871 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2872 self.priceModel.timeframe, 2873 "{hours}h {minutes}m {seconds}s", 2874 ) 2875 2876 if loadedHistory is not None and not loadedHistory.empty: 2877 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2878 len(loadedHistory), 2879 tfStr, 2880 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2881 ) 2882 2883 else: 2884 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2885 2886 else: 2887 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2888 2889 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2891 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2892 """ 2893 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2894 2895 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2896 Default: `index.html` (both for interact and non-interact candlesticks chart). 2897 2898 See also: `History()` and `LoadHistory()` methods. 2899 2900 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2901 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2902 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2903 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2904 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2905 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2906 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2907 """ 2908 if isinstance(candles, str): 2909 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2910 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2911 2912 elif isinstance(candles, pd.DataFrame): 2913 self.priceModel.prices = candles # set candles chain from variable 2914 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2915 2916 if "datetime" not in candles.columns: 2917 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2918 2919 else: 2920 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2921 raise Exception("Incorrect value") 2922 2923 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2924 2925 if interact: 2926 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2927 2928 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2929 2930 else: 2931 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2932 2933 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2934 2935 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2937 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2938 """ 2939 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2940 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2941 2942 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2943 2944 :param operation: string "Buy" or "Sell". 2945 :param lots: volume, integer count of lots >= 1. 2946 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2947 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2948 :param expDate: string "Undefined" by default or local date in future, 2949 it is a string with format `%Y-%m-%d %H:%M:%S`. 2950 :return: JSON with response from broker server. 2951 """ 2952 if self.accountId is None or not self.accountId: 2953 uLogger.error("Variable `accountId` must be defined for using this method!") 2954 raise Exception("Account ID required") 2955 2956 if operation is None or not operation or operation not in ("Buy", "Sell"): 2957 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2958 raise Exception("Incorrect value") 2959 2960 if lots is None or lots < 1: 2961 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2962 lots = 1 2963 2964 if tp is None or tp < 0: 2965 tp = 0 2966 2967 if sl is None or sl < 0: 2968 sl = 0 2969 2970 if expDate is None or not expDate: 2971 expDate = "Undefined" 2972 2973 if not (self._ticker or self._figi): 2974 uLogger.error("Ticker or FIGI must be defined!") 2975 raise Exception("Ticker or FIGI required") 2976 2977 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2978 self._ticker = instrument["ticker"] 2979 self._figi = instrument["figi"] 2980 2981 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2982 2983 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2984 self.body = str({ 2985 "figi": self._figi, 2986 "quantity": str(lots), 2987 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2988 "accountId": str(self.accountId), 2989 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2990 }) 2991 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2992 2993 if "orderId" in response.keys(): 2994 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2995 operation, response["orderId"], 2996 self._ticker, self._figi, lots, 2997 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2998 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2999 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3000 )) 3001 3002 if tp > 0: 3003 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3004 3005 if sl > 0: 3006 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3007 3008 else: 3009 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3010 3011 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3013 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3014 """ 3015 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3016 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3017 3018 See also: `Order()` and `Trade()` docstrings. 3019 3020 :param lots: volume, integer count of lots >= 1. 3021 :param tp: float > 0, take profit price of stop-order. 3022 :param sl: float > 0, stop loss price of stop-order. 3023 :param expDate: it's a local date in future. 3024 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3025 :return: JSON with response from broker server. 3026 """ 3027 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3029 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3030 """ 3031 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3032 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3033 3034 See also: `Order()` and `Trade()` docstrings. 3035 3036 :param lots: volume, integer count of lots >= 1. 3037 :param tp: float > 0, take profit price of stop-order. 3038 :param sl: float > 0, stop loss price of stop-order. 3039 :param expDate: it's a local date in the future. 3040 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3041 :return: JSON with response from broker server. 3042 """ 3043 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3045 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3046 """ 3047 Close position of given instruments. 3048 3049 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3050 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3051 This avoids unnecessary downloading data from the server. 3052 """ 3053 if instruments is None or not instruments: 3054 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3055 raise Exception("Ticker or FIGI required") 3056 3057 if isinstance(instruments, str): 3058 instruments = [instruments] 3059 3060 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3061 if uniqueInstruments: 3062 if portfolio is None or not portfolio: 3063 portfolio = self.Overview(show=False) 3064 3065 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3066 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3067 3068 for self._figi in uniqueInstruments: 3069 if self._figi not in allOpened: 3070 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3071 continue 3072 3073 # search open trade info about instrument by ticker: 3074 instrument = {} 3075 for iType in TKS_INSTRUMENTS: 3076 if instrument: 3077 break 3078 3079 for item in portfolio["stat"][iType]: 3080 if item["figi"] == self._figi: 3081 instrument = item 3082 break 3083 3084 if instrument: 3085 self._ticker = instrument["ticker"] 3086 self._figi = instrument["figi"] 3087 3088 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3089 self._ticker, 3090 self._figi, 3091 int(instrument["volume"]), 3092 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3093 )) 3094 3095 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3096 3097 if tradeLots > 0: 3098 if instrument["blocked"] > 0: 3099 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3100 instrument["blocked"], 3101 self._ticker, 3102 tradeLots, 3103 )) 3104 3105 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3106 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3107 3108 else: 3109 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3111 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3112 """ 3113 Close all positions of given instruments with defined type. 3114 3115 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3116 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3117 This avoids unnecessary downloading data from the server. 3118 """ 3119 if iType not in TKS_INSTRUMENTS: 3120 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3121 3122 else: 3123 if portfolio is None or not portfolio: 3124 portfolio = self.Overview(show=False) 3125 3126 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3127 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3128 3129 if tickers and portfolio: 3130 self.CloseTrades(tickers, portfolio) 3131 3132 else: 3133 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3135 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3136 """ 3137 Universal method to create market or limit orders with all available parameters for current `accountId`. 3138 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3139 3140 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3141 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3142 3143 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3144 then broker immediately open market order as you can do simple --buy or --sell operations! 3145 3146 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3147 When current price will go up or down to target price value then broker opens a limit order. 3148 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3149 3150 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3151 3152 :param operation: string "Buy" or "Sell". 3153 :param orderType: string "Limit" or "Stop". 3154 :param lots: volume, integer count of lots >= 1. 3155 :param targetPrice: target price > 0. This is open trade price for limit order. 3156 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3157 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3158 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3159 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3160 Stop loss order always executed by market price. 3161 :param expDate: string "Undefined" by default or local date in future. 3162 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3163 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3164 A limit order has no expiration date, it lasts until the end of the trading day. 3165 :return: JSON with response from broker server. 3166 """ 3167 if self.accountId is None or not self.accountId: 3168 uLogger.error("Variable `accountId` must be defined for using this method!") 3169 raise Exception("Account ID required") 3170 3171 if operation is None or not operation or operation not in ("Buy", "Sell"): 3172 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3173 raise Exception("Incorrect value") 3174 3175 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3176 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3177 raise Exception("Incorrect value") 3178 3179 if lots is None or lots < 1: 3180 uLogger.error("You must define trade volume > 0: integer count of lots!") 3181 raise Exception("Incorrect value") 3182 3183 if targetPrice is None or targetPrice <= 0: 3184 uLogger.error("Target price for limit-order must be greater than 0!") 3185 raise Exception("Incorrect value") 3186 3187 if limitPrice is None or limitPrice <= 0: 3188 limitPrice = targetPrice 3189 3190 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3191 stopType = "Limit" 3192 3193 if expDate is None or not expDate: 3194 expDate = "Undefined" 3195 3196 if not (self._ticker or self._figi): 3197 uLogger.error("Tocker or FIGI must be defined!") 3198 raise Exception("Ticker or FIGI required") 3199 3200 response = {} 3201 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3202 self._ticker = instrument["ticker"] 3203 self._figi = instrument["figi"] 3204 3205 if orderType == "Limit": 3206 uLogger.debug( 3207 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3208 self._ticker, self._figi, 3209 operation, lots, targetPrice, instrument["currency"], 3210 )) 3211 3212 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3213 self.body = str({ 3214 "figi": self._figi, 3215 "quantity": str(lots), 3216 "price": FloatToNano(targetPrice), 3217 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3218 "accountId": str(self.accountId), 3219 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3220 }) 3221 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3222 3223 if "orderId" in response.keys(): 3224 uLogger.info( 3225 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3226 response["orderId"], self._ticker, self._figi, operation, lots, 3227 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3228 )) 3229 3230 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3231 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3232 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3238 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3239 targetPrice, instrument["currency"], 3240 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3241 )) 3242 3243 else: 3244 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3245 3246 if orderType == "Stop": 3247 uLogger.debug( 3248 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3249 self._ticker, self._figi, 3250 operation, lots, 3251 targetPrice, instrument["currency"], 3252 limitPrice, instrument["currency"], 3253 stopType, expDate, 3254 )) 3255 3256 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3257 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3258 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3259 3260 body = { 3261 "figi": self._figi, 3262 "quantity": str(lots), 3263 "price": FloatToNano(limitPrice), 3264 "stopPrice": FloatToNano(targetPrice), 3265 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3266 "accountId": str(self.accountId), 3267 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3268 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3269 } 3270 3271 if expDateUTC: 3272 body["expireDate"] = expDateUTC 3273 3274 self.body = str(body) 3275 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3276 3277 if "stopOrderId" in response.keys(): 3278 uLogger.info( 3279 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3280 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3281 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3282 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3283 TKS_STOP_ORDER_TYPES[stopOrderType], 3284 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3285 )) 3286 3287 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3288 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3289 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3290 targetPrice, instrument["currency"], 3291 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3292 )) 3293 3294 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3295 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3296 targetPrice, instrument["currency"], 3297 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3298 )) 3299 3300 else: 3301 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3302 3303 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3305 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3306 """ 3307 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3308 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3309 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3310 See also: `Order()` docstring. 3311 3312 :param lots: volume, integer count of lots >= 1. 3313 :param targetPrice: target price > 0. This is open trade price for limit order. 3314 :return: JSON with response from broker server. 3315 """ 3316 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3318 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3319 """ 3320 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3321 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3322 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3323 target price value then broker opens a limit order. See also: `Order()` docstring. 3324 3325 :param lots: volume, integer count of lots >= 1. 3326 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3327 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3328 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3329 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3330 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3331 :param expDate: string "Undefined" by default or local date in future. 3332 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3333 This date is converting to UTC format for server. 3334 :return: JSON with response from broker server. 3335 """ 3336 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3338 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3339 """ 3340 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3341 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3342 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3343 See also: `Order()` docstring. 3344 3345 :param lots: volume, integer count of lots >= 1. 3346 :param targetPrice: target price > 0. This is open trade price for limit order. 3347 :return: JSON with response from broker server. 3348 """ 3349 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3351 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3352 """ 3353 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3354 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3355 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3356 target price value then broker opens a limit order. See also: `Order()` docstring. 3357 3358 :param lots: volume, integer count of lots >= 1. 3359 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3360 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3361 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3362 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3363 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3364 :param expDate: string "Undefined" by default or local date in future. 3365 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3366 This date is converting to UTC format for server. 3367 :return: JSON with response from broker server. 3368 """ 3369 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3371 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3372 """ 3373 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3374 3375 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3376 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3377 This avoids unnecessary downloading data from the server. 3378 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3379 """ 3380 if self.accountId is None or not self.accountId: 3381 uLogger.error("Variable `accountId` must be defined for using this method!") 3382 raise Exception("Account ID required") 3383 3384 if orderIDs: 3385 if allOrdersIDs is None: 3386 rawOrders = self.RequestPendingOrders() 3387 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3388 3389 if allStopOrdersIDs is None: 3390 rawStopOrders = self.RequestStopOrders() 3391 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3392 3393 for orderID in orderIDs: 3394 idInPendingOrders = orderID in allOrdersIDs 3395 idInStopOrders = orderID in allStopOrdersIDs 3396 3397 if not (idInPendingOrders or idInStopOrders): 3398 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3399 continue 3400 3401 else: 3402 if idInPendingOrders: 3403 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3404 3405 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3406 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3407 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3408 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3409 3410 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3411 if self.moreDebug: 3412 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3413 3414 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3415 3416 else: 3417 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3418 3419 elif idInStopOrders: 3420 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3421 3422 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3423 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3424 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3425 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3426 3427 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3428 if self.moreDebug: 3429 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3430 3431 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3432 3433 else: 3434 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3435 3436 else: 3437 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3439 def CloseAllOrders(self) -> None: 3440 """ 3441 Gets a list of open pending and stop orders and cancel it all. 3442 """ 3443 rawOrders = self.RequestPendingOrders() 3444 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3445 lenOrders = len(allOrdersIDs) 3446 3447 rawStopOrders = self.RequestStopOrders() 3448 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3449 lenSOrders = len(allStopOrdersIDs) 3450 3451 if lenOrders > 0 or lenSOrders > 0: 3452 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3453 3454 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3455 3456 else: 3457 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3459 def CloseAll(self, *args) -> None: 3460 """ 3461 Close all available (not blocked) opened trades and orders. 3462 3463 Also, you can select one or more keywords case-insensitive: 3464 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3465 3466 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3467 """ 3468 overview = self.Overview(show=False) # get all open trades info 3469 3470 if len(args) == 0: 3471 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3472 self.CloseAllOrders() # close all pending and stop orders 3473 3474 for iType in TKS_INSTRUMENTS: 3475 if iType != "Currencies": 3476 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3477 3478 else: 3479 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3480 lowerArgs = [x.lower() for x in args] 3481 3482 if "orders" in lowerArgs: 3483 self.CloseAllOrders() # close all pending and stop orders 3484 3485 for iType in TKS_INSTRUMENTS: 3486 if iType.lower() in lowerArgs and iType != "Currencies": 3487 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3489 def CloseAllByTicker(self, instrument: str) -> None: 3490 """ 3491 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3492 3493 This method searches opened trade and orders of instrument throw all portfolio and then use 3494 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3495 3496 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3497 3498 :param instrument: string with ticker. 3499 """ 3500 if instrument is None or not instrument: 3501 uLogger.error("Ticker name must be defined for using this method!") 3502 raise Exception("Ticker required") 3503 3504 overview = self.Overview(show=False) # get user portfolio with all open trades info 3505 3506 self._ticker = instrument # try to set instrument as ticker 3507 self._figi = "" 3508 3509 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3510 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3511 3512 if limitAll and self.IsInLimitOrders(portfolio=overview): 3513 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3514 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3515 3516 if stopAll and self.IsInStopOrders(portfolio=overview): 3517 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3518 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3519 3520 if self.IsInPortfolio(portfolio=overview): 3521 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3522 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3524 def CloseAllByFIGI(self, instrument: str) -> None: 3525 """ 3526 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3527 3528 This method searches opened trade and orders of instrument throw all portfolio and then use 3529 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3530 3531 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3532 3533 :param instrument: string with FIGI id. 3534 """ 3535 if instrument is None or not instrument: 3536 uLogger.error("FIGI id must be defined for using this method!") 3537 raise Exception("FIGI required") 3538 3539 overview = self.Overview(show=False) # get user portfolio with all open trades info 3540 3541 self._ticker = "" 3542 self._figi = instrument # try to set instrument as FIGI id 3543 3544 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3545 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3546 3547 if limitAll and self.IsInLimitOrders(portfolio=overview): 3548 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3549 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3550 3551 if stopAll and self.IsInStopOrders(portfolio=overview): 3552 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3553 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3554 3555 if self.IsInPortfolio(portfolio=overview): 3556 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3557 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3559 @staticmethod 3560 def ParseOrderParameters(operation, **inputParameters): 3561 """ 3562 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3563 3564 :param operation: string "Buy" or "Sell". 3565 :param inputParameters: this is dict of strings that looks like this 3566 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3567 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3568 "prices" key: one or more prices to open limit-orders 3569 Counts of values in lots and prices lists must be equals! 3570 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3571 """ 3572 # TODO: update order grid work with api v2 3573 pass 3574 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3575 # 3576 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3577 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3578 # raise Exception("Incorrect value") 3579 # 3580 # if "l" in inputParameters.keys(): 3581 # inputParameters["lots"] = inputParameters.pop("l") 3582 # 3583 # if "p" in inputParameters.keys(): 3584 # inputParameters["prices"] = inputParameters.pop("p") 3585 # 3586 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3587 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3588 # raise Exception("Incorrect value") 3589 # 3590 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3591 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3592 # 3593 # if len(lots) != len(prices): 3594 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3595 # raise Exception("Incorrect value") 3596 # 3597 # uLogger.debug("Extracted parameters for orders:") 3598 # uLogger.debug("lots = {}".format(lots)) 3599 # uLogger.debug("prices = {}".format(prices)) 3600 # 3601 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3602 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3603 # uLogger.debug("Order parameters: {}".format(result)) 3604 # 3605 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3607 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3608 """ 3609 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3610 3611 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3612 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3613 """ 3614 result = False 3615 msg = "Instrument not defined!" 3616 3617 if portfolio is None or not portfolio: 3618 portfolio = self.Overview(show=False) 3619 3620 if self._ticker: 3621 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3622 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3623 3624 for iType in TKS_INSTRUMENTS: 3625 for instrument in portfolio["stat"][iType]: 3626 if instrument["ticker"] == self._ticker: 3627 result = True 3628 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3629 break 3630 3631 elif self._figi: 3632 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3633 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3634 3635 for iType in TKS_INSTRUMENTS: 3636 for instrument in portfolio["stat"][iType]: 3637 if instrument["figi"] == self._figi: 3638 result = True 3639 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3640 break 3641 3642 else: 3643 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3644 3645 uLogger.debug(msg) 3646 3647 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3649 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3650 """ 3651 Returns instrument from the user's portfolio if it presents there. 3652 Instrument must be defined by `ticker` (highly priority) or `figi`. 3653 3654 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3655 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3656 """ 3657 result = None 3658 msg = "Instrument not defined!" 3659 3660 if portfolio is None or not portfolio: 3661 portfolio = self.Overview(show=False) 3662 3663 if self._ticker: 3664 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3665 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3666 3667 for iType in TKS_INSTRUMENTS: 3668 for instrument in portfolio["stat"][iType]: 3669 if instrument["ticker"] == self._ticker: 3670 result = instrument 3671 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3672 break 3673 3674 elif self._figi: 3675 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3676 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3677 3678 for iType in TKS_INSTRUMENTS: 3679 for instrument in portfolio["stat"][iType]: 3680 if instrument["figi"] == self._figi: 3681 result = instrument 3682 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3683 break 3684 3685 else: 3686 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3687 3688 uLogger.debug(msg) 3689 3690 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3692 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3693 """ 3694 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3695 3696 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3697 3698 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3699 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3700 """ 3701 result = False 3702 msg = "Instrument not defined!" 3703 3704 if portfolio is None or not portfolio: 3705 portfolio = self.Overview(show=False) 3706 3707 if self._ticker: 3708 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3709 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3710 3711 for instrument in portfolio["stat"]["orders"]: 3712 if instrument["ticker"] == self._ticker: 3713 result = True 3714 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3715 break 3716 3717 elif self._figi: 3718 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3719 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3720 3721 for instrument in portfolio["stat"]["orders"]: 3722 if instrument["figi"] == self._figi: 3723 result = True 3724 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3725 break 3726 3727 else: 3728 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3729 3730 uLogger.debug(msg) 3731 3732 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3734 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3735 """ 3736 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3737 Instrument must be defined by `ticker` (highly priority) or `figi`. 3738 3739 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3740 3741 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3742 :return: list with `orderID`s of limit orders. 3743 """ 3744 result = [] 3745 msg = "Instrument not defined!" 3746 3747 if portfolio is None or not portfolio: 3748 portfolio = self.Overview(show=False) 3749 3750 if self._ticker: 3751 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3752 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3753 3754 for instrument in portfolio["stat"]["orders"]: 3755 if instrument["ticker"] == self._ticker: 3756 result.append(instrument["orderID"]) 3757 3758 if result: 3759 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3760 3761 elif self._figi: 3762 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3763 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3764 3765 for instrument in portfolio["stat"]["orders"]: 3766 if instrument["figi"] == self._figi: 3767 result.append(instrument["orderID"]) 3768 3769 if result: 3770 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3771 3772 else: 3773 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3774 3775 uLogger.debug(msg) 3776 3777 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3779 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3780 """ 3781 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3782 3783 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3784 3785 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3786 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3787 """ 3788 result = False 3789 msg = "Instrument not defined!" 3790 3791 if portfolio is None or not portfolio: 3792 portfolio = self.Overview(show=False) 3793 3794 if self._ticker: 3795 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3796 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3797 3798 for instrument in portfolio["stat"]["stopOrders"]: 3799 if instrument["ticker"] == self._ticker: 3800 result = True 3801 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3802 break 3803 3804 elif self._figi: 3805 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3806 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3807 3808 for instrument in portfolio["stat"]["stopOrders"]: 3809 if instrument["figi"] == self._figi: 3810 result = True 3811 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3812 break 3813 3814 else: 3815 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3816 3817 uLogger.debug(msg) 3818 3819 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3821 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3822 """ 3823 Returns list with all `orderID`s of opened stop orders for the instrument. 3824 Instrument must be defined by `ticker` (highly priority) or `figi`. 3825 3826 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3827 3828 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3829 :return: list with `orderID`s of stop orders. 3830 """ 3831 result = [] 3832 msg = "Instrument not defined!" 3833 3834 if portfolio is None or not portfolio: 3835 portfolio = self.Overview(show=False) 3836 3837 if self._ticker: 3838 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3839 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3840 3841 for instrument in portfolio["stat"]["stopOrders"]: 3842 if instrument["ticker"] == self._ticker: 3843 result.append(instrument["orderID"]) 3844 3845 if result: 3846 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3847 3848 elif self._figi: 3849 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3850 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3851 3852 for instrument in portfolio["stat"]["stopOrders"]: 3853 if instrument["figi"] == self._figi: 3854 result.append(instrument["orderID"]) 3855 3856 if result: 3857 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3858 3859 else: 3860 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3861 3862 uLogger.debug(msg) 3863 3864 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3866 def RequestLimits(self) -> dict: 3867 """ 3868 Method for obtaining the available funds for withdrawal for current `accountId`. 3869 3870 See also: 3871 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3872 - `OverviewLimits()` method 3873 3874 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3875 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3876 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3877 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3878 """ 3879 if self.accountId is None or not self.accountId: 3880 uLogger.error("Variable `accountId` must be defined for using this method!") 3881 raise Exception("Account ID required") 3882 3883 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3884 3885 self.body = str({"accountId": self.accountId}) 3886 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3887 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3888 3889 if self.moreDebug: 3890 uLogger.debug("Records about available funds for withdrawal successfully received") 3891 3892 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3894 def OverviewLimits(self, show: bool = False) -> dict: 3895 """ 3896 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3897 3898 See also: `RequestLimits()`. 3899 3900 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3901 :return: dict with raw parsed data from server and some calculated statistics about it. 3902 """ 3903 if self.accountId is None or not self.accountId: 3904 uLogger.error("Variable `accountId` must be defined for using this method!") 3905 raise Exception("Account ID required") 3906 3907 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3908 3909 view = { 3910 "rawLimits": rawLimits, 3911 "limits": { # parsed data for every currency: 3912 "money": { # this is an array of portfolio currency positions 3913 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3914 }, 3915 "blocked": { # this is an array of blocked currency 3916 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3917 }, 3918 "blockedGuarantee": { # this is locked money under collateral for futures 3919 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3920 }, 3921 }, 3922 } 3923 3924 # --- Prepare text table with limits in human-readable format: 3925 if show: 3926 info = [ 3927 "# Withdrawal limits\n\n", 3928 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3929 "* **Account ID:** [{}]\n".format(self.accountId), 3930 ] 3931 3932 if view["limits"]["money"]: 3933 info.extend([ 3934 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3935 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3936 ]) 3937 3938 else: 3939 info.append("\nNo withdrawal limits\n") 3940 3941 for curr in view["limits"]["money"].keys(): 3942 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3943 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3944 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3945 3946 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3947 "[{}]".format(curr), 3948 "{:.2f}".format(view["limits"]["money"][curr]), 3949 "{:.2f}".format(availableMoney), 3950 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3951 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3952 ) 3953 3954 if curr == "rub": 3955 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3956 3957 else: 3958 info.append(infoStr) 3959 3960 infoText = "".join(info) 3961 3962 uLogger.info(infoText) 3963 3964 if self.withdrawalLimitsFile: 3965 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3966 fH.write(infoText) 3967 3968 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3969 3970 if self.useHTMLReports: 3971 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 3972 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 3973 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 3974 3975 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 3976 3977 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3979 def RequestAccounts(self) -> dict: 3980 """ 3981 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3982 3983 See also: 3984 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3985 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3986 - `OverviewUserInfo()` method 3987 3988 :return: dict with raw data from server that contains accounts info. Example of dict: 3989 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3990 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3991 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3992 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3993 """ 3994 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3995 3996 self.body = str({}) 3997 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3998 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3999 4000 if self.moreDebug: 4001 uLogger.debug("Records about available accounts successfully received") 4002 4003 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4005 def RequestUserInfo(self) -> dict: 4006 """ 4007 Method for requesting common user's information. 4008 4009 See also: 4010 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4011 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4012 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4013 - `OverviewUserInfo()` method 4014 4015 :return: dict with raw data from server that contains user's information. Example of dict: 4016 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4017 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4018 """ 4019 uLogger.debug("Requesting common user's information. Wait, please...") 4020 4021 self.body = str({}) 4022 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4023 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4024 4025 if self.moreDebug: 4026 uLogger.debug("Records about current user successfully received") 4027 4028 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4030 def RequestMarginStatus(self, accountId: str = None) -> dict: 4031 """ 4032 Method for requesting margin calculation for defined account ID. 4033 4034 See also: 4035 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4036 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4037 - `OverviewUserInfo()` method 4038 4039 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4040 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4041 Example of responses: 4042 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4043 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4044 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4045 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4046 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4047 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4048 """ 4049 if accountId is None or not accountId: 4050 if self.accountId is None or not self.accountId: 4051 uLogger.error("Variable `accountId` must be defined for using this method!") 4052 raise Exception("Account ID required") 4053 4054 else: 4055 accountId = self.accountId # use `self.accountId` (main ID) by default 4056 4057 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4058 4059 self.body = str({"accountId": accountId}) 4060 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4061 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4062 4063 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4064 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4065 rawMargin = {} 4066 4067 else: 4068 if self.moreDebug: 4069 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4070 4071 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4073 def RequestTariffLimits(self) -> dict: 4074 """ 4075 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4076 4077 See also: 4078 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4079 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4080 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4081 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4082 - `OverviewUserInfo()` method 4083 4084 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4085 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4086 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4087 """ 4088 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4089 4090 self.body = str({}) 4091 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4092 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4093 4094 if self.moreDebug: 4095 uLogger.debug("Records with limits of current tariff successfully received") 4096 4097 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4099 def RequestBondCoupons(self, iJSON: dict) -> dict: 4100 """ 4101 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4102 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4103 All dates are in UTC timezone. 4104 4105 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4106 Documentation: 4107 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4108 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4109 4110 See also: `ExtendBondsData()`. 4111 4112 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4113 If raw iJSON is not data of bond then server returns an error [400] with message: 4114 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4115 :return: dictionary with bond payment calendar. Response example 4116 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4117 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4118 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4119 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4120 """ 4121 if iJSON["figi"] is None or not iJSON["figi"]: 4122 uLogger.error("FIGI must be defined for using this method!") 4123 raise Exception("FIGI required") 4124 4125 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4126 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4127 4128 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4129 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4130 self._figi, 4131 startDate, 4132 endDate, 4133 )) 4134 4135 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4136 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4137 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4138 4139 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4140 uLogger.warning("Instrument type is not bond!") 4141 4142 else: 4143 if self.moreDebug: 4144 uLogger.debug("Records about bond payment calendar successfully received") 4145 4146 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4148 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4149 """ 4150 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4151 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4152 coupon yields, current yields and some statistics etc. 4153 4154 WARNING! This is too long operation if a lot of bonds requested from broker server. 4155 4156 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4157 4158 :param instruments: list of strings with tickers or FIGIs. 4159 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4160 for further used by data scientists or stock analytics. 4161 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4162 In XLSX-file and Pandas DataFrame fields mean: 4163 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4164 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4165 """ 4166 if instruments is None or not instruments: 4167 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4168 raise Exception("Ticker or FIGI required") 4169 4170 if isinstance(instruments, str): 4171 instruments = [instruments] 4172 4173 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4174 4175 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4176 4177 iCount = len(uniqueInstruments) 4178 tooLong = iCount >= 20 4179 if tooLong: 4180 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4181 4182 bonds = None 4183 for i, self._figi in enumerate(uniqueInstruments): 4184 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4185 4186 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4187 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4188 rawBond = self.SearchByFIGI(requestPrice=True) 4189 4190 # Widen raw data with UTC current time (iData["actualDateTime"]): 4191 actualDate = datetime.now(tzutc()) 4192 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4193 4194 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4195 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4196 4197 # Replace some values with human-readable: 4198 iData["nominalCurrency"] = iData["nominal"]["currency"] 4199 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4200 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4201 iData["aciCurrency"] = iData["aciValue"]["currency"] 4202 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4203 iData["issueSize"] = int(iData["issueSize"]) 4204 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4205 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4206 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4207 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4208 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4209 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4210 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4211 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4212 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4213 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4214 4215 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4216 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4217 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4218 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4219 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4220 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4221 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4222 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4223 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4224 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4225 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4226 4227 # Widen raw data with calendar data from `rawCalendar` values: 4228 calendarData = [] 4229 if "events" in iData["rawCalendar"].keys(): 4230 for item in iData["rawCalendar"]["events"]: 4231 calendarData.append({ 4232 "couponDate": item["couponDate"], 4233 "couponNumber": int(item["couponNumber"]), 4234 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4235 "payCurrency": item["payOneBond"]["currency"], 4236 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4237 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4238 "couponStartDate": item["couponStartDate"], 4239 "couponEndDate": item["couponEndDate"], 4240 "couponPeriod": item["couponPeriod"], 4241 }) 4242 4243 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4244 if "maturityDate" not in iData.keys(): 4245 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4246 4247 # Widen raw data with Coupon Rate. 4248 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4249 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4250 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4251 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4252 4253 # Widen raw data with Yield to Maturity (YTM) on current date. 4254 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4255 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4256 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4257 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4258 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4259 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4260 4261 iData["calendar"] = calendarData # adds calendar at the end 4262 4263 # Remove not used data: 4264 iData.pop("uid") 4265 iData.pop("positionUid") 4266 iData.pop("currentPrice") 4267 iData.pop("rawCalendar") 4268 4269 colNames = list(iData.keys()) 4270 if bonds is None: 4271 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4272 4273 else: 4274 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4275 4276 else: 4277 uLogger.warning("Instrument is not a bond!") 4278 4279 processed = round(100 * (i + 1) / iCount, 1) 4280 if tooLong and processed % 5 == 0: 4281 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4282 4283 else: 4284 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4285 4286 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4287 4288 # Saving bonds from Pandas DataFrame to XLSX sheet: 4289 if xlsx and self.bondsXLSXFile: 4290 with pd.ExcelWriter( 4291 path=self.bondsXLSXFile, 4292 date_format=TKS_DATE_FORMAT, 4293 datetime_format=TKS_DATE_TIME_FORMAT, 4294 mode="w", 4295 ) as writer: 4296 bonds.to_excel( 4297 writer, 4298 sheet_name="Extended bonds data", 4299 index=True, 4300 encoding="UTF-8", 4301 freeze_panes=(1, 1), 4302 ) # saving as XLSX-file with freeze first row and column as headers 4303 4304 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4305 4306 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4308 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4309 """ 4310 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4311 4312 WARNING! This is too long operation if a lot of bonds requested from broker server. 4313 4314 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4315 4316 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4317 extended information about bonds: main info, current prices, bond payment calendar, 4318 coupon yields, current yields and some statistics etc. 4319 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4320 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4321 for further used by data scientists or stock analytics. 4322 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4323 """ 4324 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4325 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4326 4327 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4328 4329 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4330 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4331 calendar = None 4332 for bond in extBonds.iterrows(): 4333 for item in bond[1]["calendar"]: 4334 cData = { 4335 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4336 "couponDate": item["couponDate"], 4337 "figi": bond[1]["figi"], 4338 "ticker": bond[1]["ticker"], 4339 "name": bond[1]["name"], 4340 "couponNumber": item["couponNumber"], 4341 "payOneBond": item["payOneBond"], 4342 "payCurrency": item["payCurrency"], 4343 "couponType": item["couponType"], 4344 "couponPeriod": item["couponPeriod"], 4345 "fixDate": item["fixDate"], 4346 "couponStartDate": item["couponStartDate"], 4347 "couponEndDate": item["couponEndDate"], 4348 } 4349 4350 if calendar is None: 4351 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4352 4353 else: 4354 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4355 4356 if calendar is not None: 4357 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4358 4359 # Saving calendar from Pandas DataFrame to XLSX sheet: 4360 if xlsx: 4361 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4362 4363 with pd.ExcelWriter( 4364 path=xlsxCalendarFile, 4365 date_format=TKS_DATE_FORMAT, 4366 datetime_format=TKS_DATE_TIME_FORMAT, 4367 mode="w", 4368 ) as writer: 4369 humanReadable = calendar.copy(deep=True) 4370 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4371 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4372 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4373 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4374 humanReadable.columns = colNames # human-readable column names 4375 4376 humanReadable.to_excel( 4377 writer, 4378 sheet_name="Bond payments calendar", 4379 index=False, 4380 encoding="UTF-8", 4381 freeze_panes=(1, 2), 4382 ) # saving as XLSX-file with freeze first row and column as headers 4383 4384 del humanReadable # release df in memory 4385 4386 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4387 4388 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4390 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4391 """ 4392 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4393 Also, creates Markdown file with calendar data, `calendar.md` by default. 4394 4395 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4396 4397 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4398 extended information about bonds: main info, current prices, bond payment calendar, 4399 coupon yields, current yields and some statistics etc. 4400 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4401 :param show: if `True` then also printing bonds payment calendar to the console, 4402 otherwise save to file `calendarFile` only. `False` by default. 4403 :return: multilines text in Markdown format with bonds payment calendar as a table. 4404 """ 4405 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4406 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4407 4408 infoText = "# Bond payments calendar\n\n" 4409 4410 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4411 4412 if not (calendar is None or calendar.empty): 4413 splitLine = "| | | | | | | | | |\n" 4414 4415 info = [ 4416 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4417 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4418 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4419 ] 4420 4421 newMonth = False 4422 notOneBond = calendar["figi"].nunique() > 1 4423 for i, bond in enumerate(calendar.iterrows()): 4424 if newMonth and notOneBond: 4425 info.append(splitLine) 4426 4427 info.append( 4428 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4429 " √" if bond[1]["paid"] else " —", 4430 bond[1]["couponDate"].split("T")[0], 4431 bond[1]["figi"], 4432 bond[1]["ticker"], 4433 bond[1]["couponNumber"], 4434 "{} {}".format( 4435 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4436 bond[1]["payCurrency"], 4437 ), 4438 bond[1]["couponType"], 4439 bond[1]["couponPeriod"], 4440 bond[1]["fixDate"].split("T")[0], 4441 ) 4442 ) 4443 4444 if i < len(calendar.values) - 1: 4445 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4446 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4447 newMonth = False if curDate.month == nextDate.month else True 4448 4449 else: 4450 newMonth = False 4451 4452 infoText += "".join(info) 4453 4454 if show: 4455 uLogger.info("{}".format(infoText)) 4456 4457 if self.calendarFile is not None: 4458 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4459 fH.write(infoText) 4460 4461 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4462 4463 if self.useHTMLReports: 4464 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4465 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4466 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4467 4468 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4469 4470 else: 4471 infoText += "No data\n" 4472 4473 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4475 def OverviewAccounts(self, show: bool = False) -> dict: 4476 """ 4477 Method for parsing and show simple table with all available user accounts. 4478 4479 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4480 4481 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4482 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4483 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4484 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4485 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4486 "closed": "—", "access": "Full access" }, ...}}` 4487 """ 4488 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4489 4490 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4491 accounts = { 4492 item["id"]: { 4493 "type": TKS_ACCOUNT_TYPES[item["type"]], 4494 "name": item["name"], 4495 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4496 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4497 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4498 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4499 } for item in rawAccounts["accounts"] 4500 } 4501 4502 # Raw and parsed data with some fields replaced in "stat" section: 4503 view = { 4504 "rawAccounts": rawAccounts, 4505 "stat": accounts, 4506 } 4507 4508 # --- Prepare simple text table with only accounts data in human-readable format: 4509 if show: 4510 info = [ 4511 "# User accounts\n\n", 4512 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4513 "| Account ID | Type | Status | Name |\n", 4514 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4515 ] 4516 4517 for account in view["stat"].keys(): 4518 info.extend([ 4519 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4520 account, 4521 view["stat"][account]["type"], 4522 view["stat"][account]["status"], 4523 view["stat"][account]["name"], 4524 ) 4525 ]) 4526 4527 infoText = "".join(info) 4528 4529 uLogger.info(infoText) 4530 4531 if self.userAccountsFile: 4532 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4533 fH.write(infoText) 4534 4535 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4536 4537 if self.useHTMLReports: 4538 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4539 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4540 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4541 4542 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4543 4544 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4546 def OverviewUserInfo(self, show: bool = False) -> dict: 4547 """ 4548 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4549 4550 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4551 4552 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4553 :return: dict with raw parsed data from server and some calculated statistics about it. 4554 """ 4555 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4556 tmpTicker = self._ticker 4557 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4558 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4559 self._ticker = tmpTicker 4560 4561 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4562 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4563 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4564 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4565 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4566 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4567 4568 # This is dict with parsed common user data: 4569 userInfo = { 4570 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4571 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4572 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4573 "tariff": rawUserInfo["tariff"], 4574 } 4575 4576 # This is an array of dict with parsed margin statuses for every account IDs: 4577 margins = {} 4578 for accountId in accounts.keys(): 4579 if rawMargins[accountId]: 4580 margins[accountId] = { 4581 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4582 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4583 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4584 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4585 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4586 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4587 "missing": missing["volume"], 4588 } 4589 4590 else: 4591 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4592 4593 unary = {} # unary-connection limits 4594 for item in rawTariffLimits["unaryLimits"]: 4595 if item["limitPerMinute"] in unary.keys(): 4596 unary[item["limitPerMinute"]].extend(item["methods"]) 4597 4598 else: 4599 unary[item["limitPerMinute"]] = item["methods"] 4600 4601 stream = {} # stream-connection limits 4602 for item in rawTariffLimits["streamLimits"]: 4603 if item["limit"] in stream.keys(): 4604 stream[item["limit"]].extend(item["streams"]) 4605 4606 else: 4607 stream[item["limit"]] = item["streams"] 4608 4609 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4610 limits = { 4611 "unary": unary, 4612 "stream": stream, 4613 } 4614 4615 # Raw and parsed data as an output result: 4616 view = { 4617 "rawUserInfo": rawUserInfo, 4618 "rawAccounts": rawAccounts, 4619 "rawMargins": rawMargins, 4620 "rawTariffLimits": rawTariffLimits, 4621 "stat": { 4622 "overview": overview, 4623 "userInfo": userInfo, 4624 "accounts": accounts, 4625 "margins": margins, 4626 "limits": limits, 4627 }, 4628 } 4629 4630 # --- Prepare text table with user information in human-readable format: 4631 if show: 4632 info = [ 4633 "# Full user information\n\n", 4634 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4635 "## Common information\n\n", 4636 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4637 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4638 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4639 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4640 "\n## User accounts\n\n", 4641 ] 4642 4643 for account in view["stat"]["accounts"].keys(): 4644 info.extend([ 4645 "### ID: [{}]\n\n".format(account), 4646 "| Parameters | Values |\n", 4647 "|----------------------|--------------------------------------------------------------|\n", 4648 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4649 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4650 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4651 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4652 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4653 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4654 ]) 4655 4656 if margins[account]: 4657 info.extend([ 4658 "| Margin status: | Enabled |\n", 4659 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4660 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4661 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4662 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4663 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4664 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4665 ]) 4666 4667 else: 4668 info.append("| Margin status: | Disabled |\n\n") 4669 4670 info.extend([ 4671 "\n## Current user tariff limits\n", 4672 "\n### See also\n", 4673 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4674 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4675 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4676 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4677 "\n### Unary limits\n", 4678 ]) 4679 4680 if unary: 4681 for key, values in sorted(unary.items()): 4682 info.append("\n* Max requests per minute: {}\n".format(key)) 4683 4684 for value in values: 4685 info.append(" - {}\n".format(value)) 4686 4687 else: 4688 info.append("\nNot available\n") 4689 4690 info.append("\n### Stream limits\n") 4691 4692 if stream: 4693 for key, values in sorted(stream.items()): 4694 info.append("\n* Max stream connections: {}\n".format(key)) 4695 4696 for value in values: 4697 info.append(" - {}\n".format(value)) 4698 4699 else: 4700 info.append("\nNot available\n") 4701 4702 infoText = "".join(info) 4703 4704 uLogger.info(infoText) 4705 4706 if self.userInfoFile: 4707 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4708 fH.write(infoText) 4709 4710 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4711 4712 if self.useHTMLReports: 4713 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4714 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4715 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4716 4717 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4718 4719 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4722class Args: 4723 """ 4724 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4725 """ 4726 def __init__(self, **kwargs): 4727 self.__dict__.update(kwargs) 4728 4729 def __getattr__(self, item): 4730 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4733def ParseArgs(): 4734 """This function get and parse command line keys.""" 4735 parser = ArgumentParser() # command-line string parser 4736 4737 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4738 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4739 4740 # --- options: 4741 4742 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4743 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4744 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4745 4746 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4747 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4748 4749 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4750 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4751 4752 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4753 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4754 4755 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4756 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4757 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4758 4759 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4760 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4761 4762 # --- commands: 4763 4764 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4765 4766 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4767 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4768 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4769 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4770 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4771 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4772 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4773 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4774 4775 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4776 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4777 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4778 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4779 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4780 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4781 4782 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4783 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4784 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4785 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4786 4787 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4788 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4789 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4790 4791 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4792 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4793 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4794 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4795 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4796 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4797 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4798 4799 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4800 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4801 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4802 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4803 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4804 4805 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4806 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4807 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4808 4809 cmdArgs = parser.parse_args() 4810 return cmdArgs
This function get and parse command line keys.
4813def Main(**kwargs): 4814 """ 4815 Main function for work with TKSBrokerAPI in the console. 4816 4817 See examples: 4818 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4819 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4820 """ 4821 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4822 4823 if args.debug_level: 4824 uLogger.level = 10 # always debug level by default 4825 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4826 4827 exitCode = 0 4828 start = datetime.now(tzutc()) 4829 uLogger.debug("=-" * 50) 4830 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4831 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4832 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4833 )) 4834 4835 # trying to calculate full current version: 4836 buildVersion = __version__ 4837 try: 4838 v = version("tksbrokerapi") 4839 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4840 4841 except Exception: 4842 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4843 4844 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4845 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4846 4847 try: 4848 if args.version: 4849 print("TKSBrokerAPI {}".format(buildVersion)) 4850 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4851 4852 else: 4853 # Init class for trading with Tinkoff Broker: 4854 trader = TinkoffBrokerServer( 4855 token=args.token, 4856 accountId=args.account_id, 4857 useCache=not args.no_cache, 4858 ) 4859 4860 # --- set some options: 4861 4862 if args.more: 4863 trader.moreDebug = True 4864 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4865 4866 if args.html: 4867 trader.useHTMLReports = True 4868 4869 if args.ticker: 4870 ticker = str(args.ticker).upper() # Tickers may be upper case only 4871 4872 if ticker in trader.aliasesKeys: 4873 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4874 4875 else: 4876 trader.ticker = ticker 4877 4878 if args.figi: 4879 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4880 4881 if args.depth is not None: 4882 trader.depth = args.depth 4883 4884 # --- do one command: 4885 4886 if args.list: 4887 if args.output is not None: 4888 trader.instrumentsFile = args.output 4889 4890 trader.ShowInstrumentsInfo(show=True) 4891 4892 elif args.list_xlsx: 4893 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4894 4895 elif args.bonds_xlsx is not None: 4896 if args.output is not None: 4897 trader.bondsXLSXFile = args.output 4898 4899 if len(args.bonds_xlsx) == 0: 4900 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4901 4902 else: 4903 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4904 4905 elif args.search: 4906 if args.output is not None: 4907 trader.searchResultsFile = args.output 4908 4909 trader.SearchInstruments(pattern=args.search[0], show=True) 4910 4911 elif args.info: 4912 if not (args.ticker or args.figi): 4913 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4914 raise Exception("Ticker or FIGI required") 4915 4916 if args.output is not None: 4917 trader.infoFile = args.output 4918 4919 if args.ticker: 4920 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4921 4922 else: 4923 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4924 4925 elif args.calendar is not None: 4926 if args.output is not None: 4927 trader.calendarFile = args.output 4928 4929 if len(args.calendar) == 0: 4930 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4931 4932 else: 4933 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4934 4935 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4936 4937 elif args.price: 4938 if not (args.ticker or args.figi): 4939 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4940 raise Exception("Ticker or FIGI required") 4941 4942 trader.GetCurrentPrices(show=True) 4943 4944 elif args.prices is not None: 4945 if args.output is not None: 4946 trader.pricesFile = args.output 4947 4948 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4949 4950 elif args.overview: 4951 if args.output is not None: 4952 trader.overviewFile = args.output 4953 4954 trader.Overview(show=True, details="full") 4955 4956 elif args.overview_digest: 4957 if args.output is not None: 4958 trader.overviewDigestFile = args.output 4959 4960 trader.Overview(show=True, details="digest") 4961 4962 elif args.overview_positions: 4963 if args.output is not None: 4964 trader.overviewPositionsFile = args.output 4965 4966 trader.Overview(show=True, details="positions") 4967 4968 elif args.overview_orders: 4969 if args.output is not None: 4970 trader.overviewOrdersFile = args.output 4971 4972 trader.Overview(show=True, details="orders") 4973 4974 elif args.overview_analytics: 4975 if args.output is not None: 4976 trader.overviewAnalyticsFile = args.output 4977 4978 trader.Overview(show=True, details="analytics") 4979 4980 elif args.overview_calendar: 4981 if args.output is not None: 4982 trader.overviewAnalyticsFile = args.output 4983 4984 trader.Overview(show=True, details="calendar") 4985 4986 elif args.deals is not None: 4987 if args.output is not None: 4988 trader.reportFile = args.output 4989 4990 if 0 <= len(args.deals) < 3: 4991 trader.Deals( 4992 start=args.deals[0] if len(args.deals) >= 1 else None, 4993 end=args.deals[1] if len(args.deals) == 2 else None, 4994 show=True, # Always show deals report in console 4995 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4996 ) 4997 4998 else: 4999 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5000 raise Exception("Incorrect value") 5001 5002 elif args.history is not None: 5003 if args.output is not None: 5004 trader.historyFile = args.output 5005 5006 if 0 <= len(args.history) < 3: 5007 dataReceived = trader.History( 5008 start=args.history[0] if len(args.history) >= 1 else None, 5009 end=args.history[1] if len(args.history) == 2 else None, 5010 interval="hour" if args.interval is None or not args.interval else args.interval, 5011 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5012 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5013 show=True, # shows all downloaded candles in console 5014 ) 5015 5016 if args.render_chart is not None and dataReceived is not None: 5017 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5018 5019 trader.ShowHistoryChart( 5020 candles=dataReceived, 5021 interact=iChart, 5022 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5023 ) 5024 5025 else: 5026 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5027 raise Exception("Incorrect value") 5028 5029 elif args.load_history is not None: 5030 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5031 5032 if args.render_chart is not None and histData is not None: 5033 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5034 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5035 5036 trader.ShowHistoryChart( 5037 candles=histData, 5038 interact=iChart, 5039 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5040 ) 5041 5042 elif args.trade is not None: 5043 if 1 <= len(args.trade) <= 5: 5044 trader.Trade( 5045 operation=args.trade[0], 5046 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5047 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5048 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5049 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5050 ) 5051 5052 else: 5053 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5054 5055 elif args.buy is not None: 5056 if 0 <= len(args.buy) <= 4: 5057 trader.Buy( 5058 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5059 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5060 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5061 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5062 ) 5063 5064 else: 5065 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5066 5067 elif args.sell is not None: 5068 if 0 <= len(args.sell) <= 4: 5069 trader.Sell( 5070 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5071 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5072 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5073 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5074 ) 5075 5076 else: 5077 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5078 5079 elif args.order: 5080 if 4 <= len(args.order) <= 7: 5081 trader.Order( 5082 operation=args.order[0], 5083 orderType=args.order[1], 5084 lots=int(args.order[2]), 5085 targetPrice=float(args.order[3]), 5086 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5087 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5088 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5089 ) 5090 5091 else: 5092 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5093 5094 elif args.buy_limit: 5095 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5096 5097 elif args.sell_limit: 5098 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5099 5100 elif args.buy_stop: 5101 if 2 <= len(args.buy_stop) <= 7: 5102 trader.BuyStop( 5103 lots=int(args.buy_stop[0]), 5104 targetPrice=float(args.buy_stop[1]), 5105 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5106 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5107 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5108 ) 5109 5110 else: 5111 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5112 5113 elif args.sell_stop: 5114 if 2 <= len(args.sell_stop) <= 7: 5115 trader.SellStop( 5116 lots=int(args.sell_stop[0]), 5117 targetPrice=float(args.sell_stop[1]), 5118 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5119 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5120 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5121 ) 5122 5123 else: 5124 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5125 5126 # elif args.buy_order_grid is not None: 5127 # # update order grid work with api v2 5128 # if len(args.buy_order_grid) == 2: 5129 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5130 # 5131 # for order in orderParams: 5132 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5133 # 5134 # else: 5135 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5136 # 5137 # elif args.sell_order_grid is not None: 5138 # # update order grid work with api v2 5139 # if len(args.sell_order_grid) >= 2: 5140 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5141 # 5142 # for order in orderParams: 5143 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5144 # 5145 # else: 5146 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5147 5148 elif args.close_order is not None: 5149 trader.CloseOrders(args.close_order) # close only one order 5150 5151 elif args.close_orders is not None: 5152 trader.CloseOrders(args.close_orders) # close list of orders 5153 5154 elif args.close_trade: 5155 if not (args.ticker or args.figi): 5156 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5157 raise Exception("Ticker or FIGI required") 5158 5159 if args.ticker: 5160 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5161 5162 else: 5163 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5164 5165 elif args.close_trades is not None: 5166 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5167 5168 elif args.close_all is not None: 5169 if args.ticker: 5170 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5171 5172 elif args.figi: 5173 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5174 5175 else: 5176 trader.CloseAll(*args.close_all) 5177 5178 elif args.limits: 5179 if args.output is not None: 5180 trader.withdrawalLimitsFile = args.output 5181 5182 trader.OverviewLimits(show=True) 5183 5184 elif args.user_info: 5185 if args.output is not None: 5186 trader.userInfoFile = args.output 5187 5188 trader.OverviewUserInfo(show=True) 5189 5190 elif args.account: 5191 if args.output is not None: 5192 trader.userAccountsFile = args.output 5193 5194 trader.OverviewAccounts(show=True) 5195 5196 else: 5197 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5198 raise Exception("There is no command to execute") 5199 5200 except Exception: 5201 trace = tb.format_exc() 5202 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5203 if e in trace: 5204 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5205 break 5206 5207 uLogger.debug(trace) 5208 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5209 exitCode = 255 # an error occurred, must be open a ticket for this issue 5210 5211 finally: 5212 finish = datetime.now(tzutc()) 5213 5214 if exitCode == 0: 5215 if args.more: 5216 uLogger.debug("All operations were finished success (summary code is 0).") 5217 5218 else: 5219 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5220 os.path.abspath(uLog.defaultLogFile), exitCode, 5221 )) 5222 5223 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5224 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5225 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5226 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5227 )) 5228 uLogger.debug("=-" * 50) 5229 5230 if not kwargs: 5231 sys.exit(exitCode) 5232 5233 else: 5234 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: